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
83 changes: 83 additions & 0 deletions .github/workflows/lifecycle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Lifecycle

# The life of a solution on the branch lifecycle, built again when the template's dev has moved: a solution
# made with the release two minors before the next one, changed by the team, then described, upgraded and
# grown by dotskit, each step a commit (.samples/lifecycle.sh). dotskit builds the solution after each command;
# the branch's own CI then runs on the result, as on every sample branch, and report.yml writes its report.
#
# Pushed with SAMPLES_DEPLOY_KEY, as regenerate.yml does: the branch carries workflow files, and a push by
# GITHUB_TOKEN would start no CI. This file lives on the default branch; generate.sh puts it back on each
# regeneration of it.

on:
schedule:
- cron: '47 3 * * *'
workflow_dispatch:
inputs:
force:
description: Build the branch again even when dev has not moved
type: boolean
default: false

permissions:
contents: read

# One run at a time: two would force-push over each other, and a report written between could be lost.
concurrency: lifecycle

env:
TEMPLATE_REPO: sawking-tech/DotNetSolutionKit
TEMPLATE_REF: dev
BRANCH: lifecycle

jobs:
check:
runs-on: ubuntu-24.04
outputs:
build: ${{ steps.moved.outputs.build }}
steps:
- id: moved
env:
GH_TOKEN: ${{ github.token }}
FORCE: ${{ inputs.force }}
run: |
want=$(gh api "repos/$TEMPLATE_REPO/commits/$TEMPLATE_REF" -q .sha)
have=$(gh api "repos/$GITHUB_REPOSITORY/contents/template-version?ref=$BRANCH" -q .content 2>/dev/null | base64 -d || echo none)
echo "$BRANCH: built from ${have:0:7}, $TEMPLATE_REF is ${want:0:7}"
if [ "$want" != "$have" ] || [ "$FORCE" = "true" ]; then echo "build=true" >> "$GITHUB_OUTPUT"; else echo "build=false" >> "$GITHUB_OUTPUT"; fi

build:
needs: check
if: needs.check.outputs.build == 'true'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
path: automation

- uses: actions/setup-dotnet@v6
with:
dotnet-version: 8.0.x

- name: The life of a solution, step by step
run: bash automation/.samples/lifecycle.sh "$TEMPLATE_REF" "$RUNNER_TEMP/lifecycle"

# The branch is replaced by the new history; the reports of earlier runs stay, as the site shows them. They
# join the last commit: a commit of their own would say [skip ci], and as the head it would start no CI.
- name: Replace the branch
env:
DEPLOY_KEY: ${{ secrets.SAMPLES_DEPLOY_KEY }}
run: |
key="$RUNNER_TEMP/deploy_key"
printf '%s\n' "$DEPLOY_KEY" > "$key" && chmod 600 "$key"
export GIT_SSH_COMMAND="ssh -i $key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
cd "$RUNNER_TEMP/lifecycle"
if git ls-remote --exit-code "git@github.com:${GITHUB_REPOSITORY}.git" "refs/heads/$BRANCH" >/dev/null; then
git fetch -q "git@github.com:${GITHUB_REPOSITORY}.git" "$BRANCH"
if git cat-file -e FETCH_HEAD:reports 2>/dev/null; then
git checkout -q FETCH_HEAD -- reports
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -q --amend --no-edit
fi
fi
git push -q --force "git@github.com:${GITHUB_REPOSITORY}.git" "HEAD:refs/heads/$BRANCH"
10 changes: 7 additions & 3 deletions .samples/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ while read -r args; do
# shellcheck disable=SC2086
dotnet new DotNetSolutionKit $args "${hive[@]}"
done < "$work/commands"
bash src/services/manual-add-projects.sh < /dev/null
# Before 2.8 a service joined All.sln by this script; since, it adds itself, and there is no script.
[ ! -f src/services/manual-add-projects.sh ] || bash src/services/manual-add-projects.sh < /dev/null

about=$(jq -r --arg b "$branch" '.[] | select(.branch == $b) | .about' "$here/variants.json")
home=$(jq -r '.[] | select(.default) | .branch' "$here/variants.json")
Expand Down Expand Up @@ -109,9 +110,9 @@ done

if [ "$(jq -r --arg b "$branch" '.[] | select(.branch == $b) | .default // false' "$here/variants.json")" = "true" ]; then
mkdir -p .samples .github/workflows
cp "$here/generate.sh" "$here/report.sh" "$here/ci-svg.py" "$here/variants.json" .samples/
cp "$here/generate.sh" "$here/lifecycle.sh" "$here/report.sh" "$here/ci-svg.py" "$here/variants.json" .samples/
python3 "$here/ci-svg.py" --legend .samples/legend.svg
cp "$here/../.github/workflows/regenerate.yml" "$here/../.github/workflows/report.yml" .github/workflows/
cp "$here/../.github/workflows/regenerate.yml" "$here/../.github/workflows/lifecycle.yml" "$here/../.github/workflows/report.yml" .github/workflows/
{
echo
echo "## The samples"
Expand All @@ -129,6 +130,9 @@ if [ "$(jq -r --arg b "$branch" '.[] | select(.branch == $b) | .default // false
echo "Once a day [regenerate](.github/workflows/regenerate.yml) checks the template's master: what reaches"
echo "master is a release, and every release branch is generated again from a new one, pushed, and its CI runs."
echo "full-dev follows dev instead, where the next release is built: it is generated again when dev has moved."
echo "[lifecycle]($repo/tree/lifecycle) is the life of a solution rather than a generation: made with the release"
echo "two minors before the next one, changed by the team, then described, upgraded and grown by dotskit, a commit"
echo "a step; [its workflow](.github/workflows/lifecycle.yml) builds it again when dev has moved."
echo "After each CI run of a branch, [report](.github/workflows/report.yml) writes the run into the branch's"
echo "reports/<branch>/ folder: tests, coverage, time and the branch's files, one file per release, and"
echo "ci.svg, the picture of its days above."
Expand Down
113 changes: 113 additions & 0 deletions .samples/lifecycle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
#
# The life of a solution, applied in one run into an empty folder, each step a commit:
#
# ./lifecycle.sh <template branch> <folder>
#
# A team made the solution with the template two minor versions before the current one, alone, as before
# dotskit; changed it; then dotskit describes it, upgrades it to the current version, adds a service, adds a
# flag to a service it has, and adds one more with --force. dotskit builds the solution after each command and
# stops the run when the build fails; at the end the team's change must still be there.
#
# The current version is the template branch's version.json; the start is the last patch of the second minor
# below it among the releases on nuget.org, across a major too (3.0 starts from the 2.x before the last one).
# dotskit and the template the solution is upgraded to are the sources of that branch: the run checks the next
# release before it is out. TEMPLATE_DIR names a checkout of the template to use instead of cloning one, for a
# run by hand.
set -euo pipefail

ref="$1" out="$2"
# The branch the result goes to, for its CI trigger and its README: lifecycle, or a branch of a rehearsal.
branch="${BRANCH:-lifecycle}"
template_repo="${TEMPLATE_REPO:-sawking-tech/DotNetSolutionKit}"
work="$(mktemp -d)"
names=(-N ST -P DotNetSolutionKit.Samples)
marker="// The team's own line, kept through every step of dotskit."

if [ -n "${TEMPLATE_DIR:-}" ]; then
sources="$TEMPLATE_DIR"
else
git clone -q --depth 1 --branch "$ref" "https://github.com/$template_repo.git" "$work/template"
sources="$work/template"
fi

# The start: among the releases below the current version, the second minor down, and its last patch.
current=$(jq -r .version "$sources/version.json")
current="${current%%-*}"
released=$(curl -fsS https://api.nuget.org/v3-flatcontainer/sawking.dotnetsolutionkit/index.json | jq -r '.versions[]')
minors=$(grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' <<< "$released" | cut -d. -f1,2 | sort -uV \
| while read -r m; do
# An if, so a minor at or above the current one ends the loop with status 0, not a silent exit.
if [ "$(printf '%s\n%s\n' "$m" "$current" | sort -V | head -1)" = "$m" ] && [ "${current#"$m".}" = "$current" ]; then echo "$m"; fi
done)
start_minor=$(tail -2 <<< "$minors" | head -1)
[ "$(wc -l <<< "$minors")" -ge 2 ] || { echo "nuget.org has fewer than two minors below $current to start from" >&2; exit 1; }
start=$(grep -E "^${start_minor//./\\.}\.[0-9]+$" <<< "$released" | sort -V | tail -1)
echo "the life of a solution: from $start, made with the template alone, to $current by dotskit"

dotnet build "$sources/tool/DotsKit" -c Release -o "$work/tool" -nologo -v q
export DOTSKIT_TEMPLATE_SOURCE="$sources/template"
dotskit() { dotnet "$work/tool/dotskit.dll" "$@"; }

mkdir -p "$out"
cd "$out"
git init -q
commit() {
git add -A
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" commit -qm "$1"
}

# The solution as a team made it before dotskit: the template alone, each service added by its script.
hive=(--debug:custom-hive "$work/hive")
dotnet new install "SawKing.DotNetSolutionKit::$start" "${hive[@]}" > /dev/null
# The solution's own command: --Solution since 2.8, -M false before it. Since 2.8 a service adds itself to
# All.sln by a command it asks to run, which --allow-scripts yes allows; a version before refuses the option.
if [ "$(printf '%s\n2.8.0\n' "$start" | sort -V | head -1)" = "2.8.0" ]; then
whole=(--Solution) scripts=(--allow-scripts yes)
else
whole=(-M false) scripts=()
fi
dotnet new DotNetSolutionKit "${names[@]}" -S Orders "${whole[@]}" --GitHubCiCd true --Messaging outbox --Storage true "${scripts[@]}" "${hive[@]}"
dotnet new DotNetSolutionKit "${names[@]}" -S Billing --Messaging outbox --Storage true "${scripts[@]}" "${hive[@]}"
# Before 2.8 a service joined All.sln by this script; since, it adds itself, and there is no script.
[ ! -f src/services/manual-add-projects.sh ] || bash src/services/manual-add-projects.sh < /dev/null
commit "Made with DotNetSolutionKit $start alone, as before dotskit"

program=$(ls src/services/*.Orders/*.Orders.API/Program.cs)
printf '\n%s\n' "$marker" >> "$program"
commit "The team's own change"

step() {
local message="$1"
shift
dotskit "$@" --yes
commit "$message"
}
# A solution before 2.7 does not say its version, and the run knows it: it made the solution.
step "dotskit init: the manifest of a solution made without dotskit" init --template-version "$start"
step "dotskit upgrade: from $start to $current" upgrade
step "dotskit new: a service, Catalog" new -S Catalog --MongoDB true --Notify email
step "dotskit new: a flag of a service it has, ClickHouse for Orders" new -S Orders --ClickHouse true
step "dotskit new --force: feature flags for Billing" new -S Billing --FeatureFlags true --force

grep -qF "$marker" "$program" || { echo "the team's line in $program is gone" >&2; exit 1; }
echo "the solution went from $start to $current, grew, and keeps the team's change"

# The branch's own CI runs on a push to it, as on a sample branch (generate.sh); and it says what it is.
for wf in .github/workflows/*.yml; do
sed -i "s/branches: \[main, master\]/branches: [main, master, $branch]/" "$wf"
done
commit_of=$(git -C "$sources" rev-parse HEAD 2>/dev/null || echo "$ref")
echo "$commit_of" > template-version
{
echo "# Sample: $branch"
echo
echo "The life of a solution, each step a commit of this branch: made with DotNetSolutionKit $start alone, as"
echo "before dotskit; the team's own change; then dotskit describes it (init), upgrades it to $current, adds a"
echo "service, adds a flag to a service it has, and adds one more with --force. dotskit built the solution after"
echo "each command, and the team's change is still in Orders. The CI of this branch runs on the result."
echo
echo "Built from the template's $ref at ${commit_of:0:7}. Nothing here is edited by hand: the branch is replaced"
echo "when the template moves on."
} > README.md
commit "The branch's CI, its README and what it was built from"