Skip to content
Open
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
80 changes: 80 additions & 0 deletions .github/workflows/pr-prerelease.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: PR Prerelease

on:
pull_request:
branches:
- 'release-*'
types: [opened, synchronize, reopened]

jobs:
prerelease:
name: Tag and Publish RC
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}

- name: Determine RC version
id: version
run: |
BASE_BRANCH="${{ github.base_ref }}"
XY="${BASE_BRANCH#release-}"
X="${XY%%.*}"
Y="${XY##*.}"

LATEST_RELEASE=$(git tag -l "v${X}.${Y}.*" | grep -v -- '-rc\.' | sort -V | tail -1)
if [ -z "$LATEST_RELEASE" ]; then
NEXT_Z=0
else
CURRENT_Z=$(echo "$LATEST_RELEASE" | sed "s/v${X}\.${Y}\.\([0-9]*\)/\1/")
NEXT_Z=$((CURRENT_Z + 1))
fi

RC_INT=$(git tag -l "v${X}.${Y}.${NEXT_Z}-rc.*" | wc -l | tr -d ' ')

echo "tag=v${X}.${Y}.${NEXT_Z}-rc.${RC_INT}" >> $GITHUB_OUTPUT
echo "version=${X}.${Y}.${NEXT_Z}-rc.${RC_INT}" >> $GITHUB_OUTPUT

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
source-url: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json
env:
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Add Keyfactor NuGet source credentials
run: dotnet nuget update source Keyfactor --username ${{ github.actor }} --password ${{ secrets.V2BUILDTOKEN }} --store-password-in-clear-text

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Pack
run: dotnet pack aws-auth-library/aws-auth-library.csproj --no-restore --configuration Release -p:Version=${{ steps.version.outputs.version }} --output ./nupkg

- name: Create and push RC tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag ${{ steps.version.outputs.tag }}
git push origin ${{ steps.version.outputs.tag }}

- name: Create GitHub Prerelease
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.tag }}
prerelease: true
generate_release_notes: true
files: ./nupkg/*.nupkg

- name: Push to GitHub Packages
run: dotnet nuget push ./nupkg/*.nupkg --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --skip-duplicate
69 changes: 34 additions & 35 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,40 @@ name: Release and Publish

on:
push:
tags: [ 'v*' ]
branches:
- 'release-*'

jobs:
build:
name: Build
runs-on: ubuntu-latest
permissions:
packages: read
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'

- name: Add Keyfactor NuGet source credentials
run: dotnet nuget update source Keyfactor --username ${{ github.actor }} --password ${{ secrets.V2BUILDTOKEN }} --store-password-in-clear-text

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

release:
name: Release and Publish
name: Tag and Publish Release
runs-on: ubuntu-latest
needs: build
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set version from tag
- name: Determine release version
id: version
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
if [[ "$VERSION" == *-rc* ]]; then
echo "IS_PRERELEASE=true" >> $GITHUB_ENV
BRANCH="${GITHUB_REF#refs/heads/}"
XY="${BRANCH#release-}"
X="${XY%%.*}"
Y="${XY##*.}"

LATEST_RELEASE=$(git tag -l "v${X}.${Y}.*" | grep -v -- '-rc\.' | sort -V | tail -1)
if [ -z "$LATEST_RELEASE" ]; then
NEXT_Z=0
else
echo "IS_PRERELEASE=false" >> $GITHUB_ENV
CURRENT_Z=$(echo "$LATEST_RELEASE" | sed "s/v${X}\.${Y}\.\([0-9]*\)/\1/")
NEXT_Z=$((CURRENT_Z + 1))
fi

echo "tag=v${X}.${Y}.${NEXT_Z}" >> $GITHUB_OUTPUT
echo "version=${X}.${Y}.${NEXT_Z}" >> $GITHUB_OUTPUT

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
Expand All @@ -61,15 +50,25 @@ jobs:
- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Pack
run: dotnet pack aws-auth-library/aws-auth-library.csproj --no-restore --configuration Release -p:Version=${{ env.PACKAGE_VERSION }} --output ./nupkg
run: dotnet pack aws-auth-library/aws-auth-library.csproj --no-restore --configuration Release -p:Version=${{ steps.version.outputs.version }} --output ./nupkg

- name: Create and push release tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag ${{ steps.version.outputs.tag }}
git push origin ${{ steps.version.outputs.tag }}

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
prerelease: ${{ env.IS_PRERELEASE }}
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.tag }}
prerelease: false
generate_release_notes: true
files: ./nupkg/*.nupkg

Expand Down
103 changes: 103 additions & 0 deletions CICD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# CI/CD

## Branch strategy

Release branches follow the naming convention `release-X.Y` where `X` is the major version and `Y` is the minor version. Each branch owns its own patch series independently — `release-1.0` produces `v1.0.*` tags, `release-1.1` produces `v1.1.*` tags, and so on. This allows security fixes and hotfixes to be shipped for older minor versions without disturbing newer ones.

The `main` branch is the integration target for feature development. Work flows from feature branches → `main` → `release-X.Y` when a version is being prepared for release.

## Workflows

```mermaid
flowchart TD
DEV([Developer])

DEV -->|push to any branch| BUILD_T
DEV -->|open PR targeting release-X.Y\nor push to open PR| RC_T
DEV -->|merge PR into release-X.Y| REL_T

subgraph BUILD_T ["build.yml — all branches"]
B1[Restore] --> B2[Build]
end

subgraph RC_T ["pr-prerelease.yml — PR → release-X.Y"]
direction TB
RC_V["Z = highest non-RC vX.Y.* patch + 1
N = count of existing vX.Y.Z-rc.* tags"]
RC_V --> RC1[Restore → Build → Pack\nvX.Y.Z-rc.N]
RC1 --> RC2[Push tag vX.Y.Z-rc.N\nto PR head SHA]
RC2 --> RC3[GitHub Prerelease]
RC2 --> RC4[NuGet Package]
end

subgraph REL_T ["release.yml — push to release-X.Y"]
direction TB
R_V["Z = highest non-RC vX.Y.* patch + 1\n(RC tags ignored)"]
R_V --> R1[Restore → Build → Pack\nvX.Y.Z]
R1 --> R2[Push tag vX.Y.Z]
R2 --> R3[GitHub Release]
R2 --> R4[NuGet Package]
end
```

| Workflow | File | Trigger | Purpose |
|---|---|---|---|
| Build | `build.yml` | Push or PR to any branch | Verifies the project compiles — no publish |
| PR Prerelease | `pr-prerelease.yml` | PR opened/updated targeting `release-*` | Tags and publishes an RC on every push |
| Release | `release.yml` | Push/merge to `release-*` | Tags and publishes the final release |

## Example lifecycle

```mermaid
timeline
title PR targeting release-1.0 (latest tag v1.0.0)
PR opened : v1.0.1-rc.0 prerelease created
Push to PR : v1.0.1-rc.1 prerelease created
Push to PR : v1.0.1-rc.2 prerelease created
PR merged : v1.0.1 release created
Next PR opened : v1.0.2-rc.0 prerelease created
```

## Versioning rules

| Component | Rule |
|---|---|
| `Z` (patch) | Highest non-RC `vX.Y.*` tag + 1. Zero if no releases exist yet for this `X.Y`. |
| `N` (RC int) | Count of existing `vX.Y.Z-rc.*` tags. Zero-based — resets to 0 each time `Z` advances. |
| Tag scope | `vX.Y.*` glob is anchored to the exact major and minor from the branch name. Tags from other `X.Y` series are invisible. |
| RC tags | Excluded from `Z` computation in both workflows. Only shipped releases drive the next patch number. |

## Shipping a hotfix or security patch

Because each `release-X.Y` branch is independent, patching an older version does not require touching newer ones:

1. Check out the target release branch: `git checkout release-X.Y`
2. Create a fix branch off it: `git checkout -b fix/description`
3. Commit the fix and open a PR **targeting `release-X.Y`** (not `main`)
4. Each push to the PR automatically publishes an RC prerelease for validation
5. Merge the PR — the release workflow publishes `vX.Y.Z` and the NuGet package

If the fix also applies to `main` or other release branches, cherry-pick it separately after merging.

## Required secrets

| Secret | Used by | Purpose |
|---|---|---|
| `V2BUILDTOKEN` | All workflows | Authenticates the private Keyfactor NuGet source |
| `GITHUB_TOKEN` | `pr-prerelease.yml`, `release.yml` | Pushes tags and publishes GitHub releases and packages (provided automatically by Actions) |

## Known limitations

**Concurrent PRs:** if two PRs targeting the same `release-X.Y` branch have commits pushed at the exact same moment, both workflow runs may compute the same RC tag and the second push will fail. This is inherent to the count-based tagging approach and is unlikely in practice. Re-running the failed workflow run resolves it.

**Direct pushes to release branches:** a direct push to `release-X.Y` (without a PR) triggers `release.yml` and creates a new release tag, the same as a merge. Avoid pushing directly to release branches unless intentional.

## Testing the version logic

The version computation can be validated locally without a git repo, network access, or any commits:

```sh
bash scripts/test-semver.sh
```

This runs 23 test cases covering RC and release versioning, cross-branch tag isolation, double-digit patch numbers, and a full end-to-end PR lifecycle. If the shell logic in the workflows is ever modified, update the matching functions in `scripts/test-semver.sh` and re-run.
Loading
Loading