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
75 changes: 75 additions & 0 deletions .github/actions/git-identity/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Resolve git identity

description: >
Resolves the identity of the person who triggered the run, so release commits
and tags are attributed to them rather than to an anonymous bot.

inputs:
actor:
description: 'GitHub login to attribute the release to.'
required: false
default: ${{ github.triggering_actor }}
token:
description: 'Token used to look the actor up.'
required: false
default: ${{ github.token }}

outputs:
actor-name:
description: 'Name to use for the triggering user, or the bot if unresolvable.'
value: ${{ steps.resolve.outputs.actor-name }}
actor-email:
description: 'No-reply email for the triggering user, or the bot if unresolvable.'
value: ${{ steps.resolve.outputs.actor-email }}
bot-name:
description: 'Name to record as the committer of automated commits.'
value: ${{ steps.resolve.outputs.bot-name }}
bot-email:
description: 'Email to record as the committer of automated commits.'
value: ${{ steps.resolve.outputs.bot-email }}

runs:
using: "composite"
steps:
- name: Resolve identity
shell: bash
id: resolve
env:
ACTOR: ${{ inputs.actor }}
GH_TOKEN: ${{ inputs.token }}
run: |
set -euo pipefail

# 41898282 is the fixed id of the github-actions[bot] account.
bot_name='github-actions[bot]'
bot_email='41898282+github-actions[bot]@users.noreply.github.com'

actor_name="$bot_name"
actor_email="$bot_email"

# Attribution is a nicety, so anything unresolvable here falls back to the
# bot rather than failing the release. A run started by a schedule has no
# actor, and a login can belong to a deleted account or be a bot itself.
if [[ -z "$ACTOR" ]]; then
echo 'No triggering actor; attributing this release to the bot.'
else
# GitHub links a commit to an account through the numeric user id in the
# no-reply address, so the login alone is not enough.
actor_id=$(gh api "users/${ACTOR}" --jq '.id' 2>/dev/null || true)

# A failed call can still print an error body to stdout, so require a
# bare number rather than just a non-empty value.
if [[ ! "$actor_id" =~ ^[0-9]+$ ]]; then
echo "::warning::Could not resolve a user id for '${ACTOR}'; attributing this release to the bot."
else
actor_name="$ACTOR"
actor_email="${actor_id}+${ACTOR}@users.noreply.github.com"
echo "Attributing this release to ${ACTOR} (id ${actor_id})"
fi
fi

echo "actor-name=${actor_name}" >> "$GITHUB_OUTPUT"
echo "actor-email=${actor_email}" >> "$GITHUB_OUTPUT"

echo "bot-name=${bot_name}" >> "$GITHUB_OUTPUT"
echo "bot-email=${bot_email}" >> "$GITHUB_OUTPUT"
88 changes: 88 additions & 0 deletions .github/actions/next-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Resolve next version

description: 'Reads the current SDK version and resolves the next one for the given strategy.'

inputs:
version-strategy:
description: 'patch, minor, major or explicit.'
required: true
version:
description: 'The exact version to release (no "v" prefix). Only used when version-strategy is "explicit".'
required: false
default: ''

outputs:
current-version:
description: 'The version currently committed to the repo, without a "v" prefix.'
value: ${{ steps.resolve.outputs.current-version }}
next-version:
description: 'The resolved next version, without a "v" prefix.'
value: ${{ steps.resolve.outputs.next-version }}
next-tag:
description: 'The resolved next version, with a "v" prefix.'
value: ${{ steps.resolve.outputs.next-tag }}

runs:
using: "composite"
steps:
- name: Resolve version
shell: bash
id: resolve
# Passed as env rather than interpolated into the script so the values
# are treated strictly as data.
env:
VERSION_STRATEGY: ${{ inputs.version-strategy }}
REQUESTED_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail

# constructor.io.csproj holds the <Version> that ships to NuGet, so it is
# the source of truth for what is currently released.
current=$(sed -n 's|.*<Version>\(.*\)</Version>.*|\1|p' src/constructor.io/constructor.io.csproj | head -1)

if [[ ! "$current" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Could not read a semver <Version> from src/constructor.io/constructor.io.csproj (got '$current')"
exit 1
fi

IFS=. read -r major minor patch <<< "$current"

case "$VERSION_STRATEGY" in
major)
next="$((major + 1)).0.0"
;;
minor)
next="${major}.$((minor + 1)).0"
;;
patch)
next="${major}.${minor}.$((patch + 1))"
;;
explicit)
next=$(echo "$REQUESTED_VERSION" | tr -d 'v')

if [[ ! "$next" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::version-strategy is 'explicit' but 'version' is not a semver value (got '$REQUESTED_VERSION')"
exit 1
fi
;;
*)
echo "::error::Unknown version-strategy '$VERSION_STRATEGY'"
exit 1
;;
esac

if [[ "$next" == "$current" ]]; then
echo "::error::Next version ($next) matches the current version. Nothing to release."
exit 1
fi

if git rev-parse -q --verify "refs/tags/v${next}" >/dev/null; then
echo "::error::Tag v${next} already exists."
exit 1
fi

echo "Bumping $current -> $next"

echo "current-version=$current" >> "$GITHUB_OUTPUT"
echo "next-version=$next" >> "$GITHUB_OUTPUT"
echo "next-tag=v$next" >> "$GITHUB_OUTPUT"
75 changes: 75 additions & 0 deletions .github/actions/set-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Set version

description: 'Writes a new version into every file that tracks the SDK version.'

inputs:
version:
description: 'The new version, without a "v" prefix.'
required: true

runs:
using: "composite"
steps:
- name: Write version to all tracked files
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail

version="$VERSION"

if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::'version' is not a semver value (got '$version')"
exit 1
fi

# The client sends this to the API as the version identifier.
sed -i -E "s|(private readonly string Version = \")cionet-[0-9]+\.[0-9]+\.[0-9]+(\";)|\1cionet-${version}\2|" \
src/constructor.io/client/ConstructorIO.cs

# <Version> is what NuGet publishes; <ReleaseVersion> is kept in step with it.
sed -i -E "s|(<Version>)[0-9]+\.[0-9]+\.[0-9]+(</Version>)|\1${version}\2|" \
src/constructor.io/constructor.io.csproj
sed -i -E "s|(<ReleaseVersion>)[0-9]+\.[0-9]+\.[0-9]+(</ReleaseVersion>)|\1${version}\2|" \
src/constructor.io/constructor.io.csproj
sed -i -E "s|(<ReleaseVersion>)[0-9]+\.[0-9]+\.[0-9]+(</ReleaseVersion>)|\1${version}\2|" \
src/Constructorio_NET.Tests/Constructorio_NET.Tests.csproj

sed -i -E "s|(version = )[0-9]+\.[0-9]+\.[0-9]+|\1${version}|" \
src/Constructorio_Net.sln

- name: Verify no stale versions remain
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail

version="$VERSION"

# Every one of these must now report the new version, otherwise a sed
# above silently matched nothing and we would ship a half-bumped release.
expected=(
"src/constructor.io/client/ConstructorIO.cs:cionet-${version}"
"src/constructor.io/constructor.io.csproj:<Version>${version}</Version>"
"src/constructor.io/constructor.io.csproj:<ReleaseVersion>${version}</ReleaseVersion>"
"src/Constructorio_NET.Tests/Constructorio_NET.Tests.csproj:<ReleaseVersion>${version}</ReleaseVersion>"
"src/Constructorio_Net.sln:version = ${version}"
)

failed=0

for entry in "${expected[@]}"; do
file="${entry%%:*}"
needle="${entry#*:}"

if grep -qF "$needle" "$file"; then
echo " ok $file -> $needle"
else
echo "::error file=$file::Expected to find '$needle' after the version bump"
failed=1
fi
done

exit "$failed"
44 changes: 44 additions & 0 deletions .github/workflows/create-github-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: 'Publish: Create GitHub release'

# We need these permissions to create new GitHub releases.
permissions:
contents: write

on:
workflow_call:
inputs:
version:
type: string
description: 'Tag to create the release from, including the "v" prefix.'
required: true
title:
type: string
description: 'Release title.'
required: true

# This workflow is intended to be called from the primary publish workflow
# automatically. The manual trigger is maintained for partially failed workflows
# or edge cases that require this be run independently.
workflow_dispatch:
inputs:
version:
type: string
description: 'Tag to create the release from, including the "v" prefix.'
required: true
title:
type: string
description: 'Release title.'
required: true

jobs:
create-github-release:
name: Create GitHub release
runs-on: ubuntu-latest
steps:
- name: Create release
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
with:
name: ${{ inputs.title }}
tag: ${{ inputs.version }}
token: ${{ github.token }}
generateReleaseNotes: true
Loading
Loading