From 7e80226bf244a1d894ba8889181518ca30488d76 Mon Sep 17 00:00:00 2001 From: Faris Laptop Date: Mon, 20 Jul 2026 16:12:21 +0500 Subject: [PATCH] ci: release from the production branch, version from the project file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two branches: dev for everyday work, production for releases. - ci.yml builds and packs on dev and on pull requests; it never publishes. - release.yml runs on a push (usually a merge) into production: reads from the csproj, refuses to proceed if that tag already exists, publishes to nuget.org, and only then creates the tag and the Gitea release — so a tag always means a published package. - Drop --skip-duplicate: a duplicate push must fail loudly instead of reporting success while shipping nothing. --- .gitea/workflows/ci.yml | 45 ++--------------- .gitea/workflows/release.yml | 96 ++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 42 ++++++++++++---- 3 files changed, 133 insertions(+), 50 deletions(-) create mode 100644 .gitea/workflows/release.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index cf430fc..9f38ddf 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,11 +1,12 @@ name: CI +# Everyday checks. Runs on the working branch and on pull requests into it. +# Releasing is a separate workflow driven by the production branch. on: push: - branches: [main] - tags: ["v*"] + branches: [dev] pull_request: - branches: [main] + branches: [dev, production] jobs: build: @@ -20,7 +21,7 @@ jobs: - name: Restore run: dotnet restore - # Directory.Build.props turns warnings into errors, so this also gates style. + # Directory.Build.props turns warnings into errors, so this gates style too. - name: Build run: dotnet build --configuration Release --no-restore @@ -32,39 +33,3 @@ jobs: with: name: nupkg path: artifacts/* - - publish: - # Tag pushes only: v1.2.3 publishes version 1.2.3 to nuget.org. - needs: build - if: startsWith(gitea.ref, 'refs/tags/v') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "10.0.x" - - - name: Derive version from tag - id: version - run: echo "value=${GITEA_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - env: - GITEA_REF: ${{ gitea.ref }} - - - name: Pack - run: > - dotnet pack src/CertifiEd.Client/CertifiEd.Client.csproj - --configuration Release - -p:Version=${{ steps.version.outputs.value }} - --output artifacts - - # NUGET_API_KEY must be added as a repository secret. Without it the step - # fails loudly rather than silently shipping nothing. - - name: Push to nuget.org - run: > - dotnet nuget push "artifacts/*.nupkg" - --source https://api.nuget.org/v3/index.json - --api-key "$NUGET_API_KEY" - --skip-duplicate - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..f03f851 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,96 @@ +name: Release + +# Merging (or pushing) into production releases whatever version the files +# declare: in src/CertifiEd.Client/CertifiEd.Client.csproj is the +# single source of truth. The workflow tags it, publishes the package to +# nuget.org and creates the Gitea release. +# +# To ship: bump on dev, merge dev -> production. +on: + push: + branches: [production] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Read version from the project file + id: version + run: | + set -euo pipefail + VERSION=$(sed -n 's:.*\(.*\).*:\1:p' src/CertifiEd.Client/CertifiEd.Client.csproj | head -1) + if [ -z "$VERSION" ]; then + echo "::error:: not found in src/CertifiEd.Client/CertifiEd.Client.csproj" + exit 1 + fi + echo "value=$VERSION" >> "$GITHUB_OUTPUT" + echo "Releasing version $VERSION" + + # A published NuGet version can never be replaced, so refuse to re-run an + # already released version instead of silently doing nothing. + - name: Refuse to release an existing version + env: + TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + set -euo pipefail + TAG="v${{ steps.version.outputs.value }}" + CODE=$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: token $TOKEN" \ + "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/tags/$TAG") + if [ "$CODE" = "200" ]; then + echo "::error::$TAG is already released. Bump in the csproj on dev, then merge again." + exit 1 + fi + + - name: Build + run: dotnet build --configuration Release + + - name: Pack + run: > + dotnet pack src/CertifiEd.Client/CertifiEd.Client.csproj + --configuration Release --no-build --output artifacts + + # No --skip-duplicate: a duplicate must fail loudly, not look successful. + - name: Publish to nuget.org + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + set -euo pipefail + if [ -z "${NUGET_API_KEY:-}" ]; then + echo "::error::NUGET_API_KEY secret is not set" + exit 1 + fi + dotnet nuget push artifacts/*.nupkg \ + --source https://api.nuget.org/v3/index.json \ + --api-key "$NUGET_API_KEY" + + # Tag and release only after the package is actually on nuget.org, so the + # tag always corresponds to something published. + - name: Tag and create the release + env: + TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.value }}" + TAG="v$VERSION" + BODY="CertifiEd.Client $VERSION\n\nnuget: https://www.nuget.org/packages/CertifiEd.Client/$VERSION" + curl -sf -X POST \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"$TAG\",\"target_commitish\":\"$GITHUB_SHA\",\"name\":\"$TAG\",\"body\":\"$BODY\"}" \ + "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" > /dev/null + echo "Released $TAG" + + - name: Upload package artifacts + uses: actions/upload-artifact@v3 + with: + name: nupkg + path: artifacts/* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2075620..f538cce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,12 @@ # Разработка и релиз +## Ветки + +| Ветка | Назначение | +|---|---| +| `dev` | Основная ветка разработки. Сюда идут все изменения и pull request'ы. CI собирает и пакует, но ничего не публикует. | +| `production` | Релизная ветка. Мерж сюда = выпуск версии. | + ## Сборка ```bash @@ -11,22 +18,37 @@ dotnet pack src/CertifiEd.Client/CertifiEd.Client.csproj -c Release -o artifacts ## Релиз -Версия пакета берётся из тега: +Версия берётся **из файлов** — единственный источник правды — `` в +`src/CertifiEd.Client/CertifiEd.Client.csproj`. ```bash -# 1. Обновить в src/CertifiEd.Client/CertifiEd.Client.csproj -# 2. Закоммитить и поставить тег -git tag v1.0.1 -git push origin main --tags +# 1. на dev: поднять версию +# 1.0.1 +git commit -am "chore: bump version to 1.0.1" +git push origin dev + +# 2. влить dev в production — это и есть релиз +git checkout production +git merge --no-ff dev +git push origin production ``` -CI (`.gitea/workflows/ci.yml`) на теге `v*` собирает пакет с версией из тега и публикует его на nuget.org. +Дальше `.gitea/workflows/release.yml` сам: -### Что нужно один раз настроить +1. читает версию из csproj; +2. **отказывается** публиковать, если тег `vX.Y.Z` уже существует (опубликованную версию в NuGet заменить нельзя — нужно поднять версию); +3. собирает и пакует; +4. публикует пакет на nuget.org (дубликат падает с ошибкой, а не «молча успешно»); +5. только после успешной публикации создаёт тег `vX.Y.Z` и Gitea-release. -- В настройках репозитория → Actions → Secrets добавить **`NUGET_API_KEY`** — ключ nuget.org с правом публикации пакета `CertifiEd.Client`. -- Убедиться, что к репозиторию подключён Actions-раннер (Settings → Actions → Runners). Без раннера workflow не запустится. +Порядок намеренный: тег появляется лишь тогда, когда пакет реально опубликован. + +### Что настроено один раз + +- Секрет **`NUGET_API_KEY`** — ключ nuget.org с правом публикации `CertifiEd.Client`. +- Actions-раннер с лейблом `ubuntu-latest`. +- `GITEA_TOKEN` выдаётся Actions автоматически — используется для тега и релиза. ## Совместимость -Публичный API следует semver: ломающие изменения — только в мажорной версии. Формат токена лицензии и протокол активации описаны в документации платформы CertifiEd. +Публичный API следует semver: ломающие изменения — только в мажорной версии.