diff --git a/.github/workflows/build-and-push-docker.yml b/.github/workflows/build-and-push-docker.yml index e4bffabe..e541fe28 100644 --- a/.github/workflows/build-and-push-docker.yml +++ b/.github/workflows/build-and-push-docker.yml @@ -1,14 +1,14 @@ -name: CI +name: Docker on: push: branches: - - "main" + - main tags: - "v*.*.*" pull_request: branches: - - "main" + - main env: REGISTRY: ghcr.io @@ -20,6 +20,8 @@ jobs: permissions: contents: read packages: write + attestations: write + id-token: write steps: - name: Checkout repository @@ -27,7 +29,11 @@ jobs: with: submodules: recursive + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to the Container registry + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} @@ -39,18 +45,29 @@ jobs: uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - # generate Docker tags based on the following events/attributes tags: | type=ref,event=pr type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=canary,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - name: Build and push Docker image - uses: docker/build-push-action@v5 + id: push + uses: docker/build-push-action@v6 with: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Generate artifact attestation + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f0806156..f8bcd037 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -3,6 +3,8 @@ name: CI on: push: branches: [ main ] + tags: + - "v*.*.*" pull_request: branches: [ main ] diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 00000000..65dbc791 --- /dev/null +++ b/.github/workflows/canary.yml @@ -0,0 +1,93 @@ +name: Canary + +on: + workflow_run: + workflows: ["CI"] + branches: [main] + types: [completed] + +permissions: + contents: write + actions: read + +jobs: + canary-release: + name: Publish Canary Release + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Get last release tag + id: last_tag + run: | + tag=$(git describe --tags --abbrev=0 --match "v*.*.*" 2>/dev/null || echo "none") + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Generate changelog since last release tag + uses: orhun/git-cliff-action@v4 + id: cliff + with: + config: cliff.toml + args: --unreleased --strip header + env: + OUTPUT: CHANGES.md + GITHUB_REPO: ${{ github.repository }} + + - name: Prepend header to changelog + run: | + last="${{ steps.last_tag.outputs.tag }}" + sha="${{ github.event.workflow_run.head_sha }}" + short="${sha:0:7}" + if [ "$last" != "none" ]; then + header="Changes since **$last** ([full diff](https://github.com/${{ github.repository }}/compare/${last}...${sha}))\n\n" + else + header="Changes up to \`${short}\`\n\n" + fi + printf "%b" "$header" | cat - CHANGES.md > CHANGES.tmp && mv CHANGES.tmp CHANGES.md + + - name: Download artifacts from CI run + uses: dawidd6/action-download-artifact@v6 + with: + run_id: ${{ github.event.workflow_run.id }} + path: artifacts/ + + - name: Package artifacts + run: | + declare -A platform_map=( + ["build-windows-2022"]="darkflame-universe-windows" + ["build-ubuntu-22.04"]="darkflame-universe-linux" + ["build-macos-15-intel"]="darkflame-universe-macos" + ) + cd artifacts + for dir in */; do + name="${dir%/}" + out="${platform_map[$name]:-$name}" + zip -r "../${out}.zip" "$dir" + done + cd .. + ls -lh *.zip + + - name: Delete existing canary release + run: gh release delete canary --yes --cleanup-tag 2>/dev/null || true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create canary pre-release + uses: ncipollo/release-action@v1 + with: + tag: canary + name: "Canary ${{ steps.last_tag.outputs.tag }}+${{ github.event.workflow_run.head_sha }}" + bodyFile: CHANGES.md + artifacts: "*.zip" + artifactContentType: application/zip + token: ${{ secrets.GITHUB_TOKEN }} + prerelease: true + draft: false + allowUpdates: true + removeArtifacts: true + commit: ${{ github.event.workflow_run.head_sha }} diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 00000000..e35554da --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,37 @@ +name: PR Title Check + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + check-title: + name: Conventional Commit Title + runs-on: ubuntu-latest + + steps: + - name: Check PR title follows Conventional Commits + uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + perf + refactor + docs + chore + test + ci + revert + requireScope: false + subjectPattern: ^.+$ + subjectPatternError: | + The PR title "{title}" must have a description after the type/scope prefix. + Example: "feat: add new login flow" or "fix(auth): handle null token" + wip: true + validateSingleCommit: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..fe21f22c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +name: Release + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +permissions: + contents: write + actions: read + +jobs: + release: + name: Create Release + # Only run when CI completed successfully on a version tag + if: | + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'v') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Generate changelog + uses: orhun/git-cliff-action@v4 + id: cliff + with: + config: cliff.toml + args: --latest --strip header + env: + OUTPUT: CHANGES.md + GITHUB_REPO: ${{ github.repository }} + + - name: Download artifacts from CI run + uses: dawidd6/action-download-artifact@v6 + with: + run_id: ${{ github.event.workflow_run.id }} + path: artifacts/ + + - name: Package artifacts + run: | + declare -A platform_map=( + ["build-windows-2022"]="darkflame-universe-windows" + ["build-ubuntu-22.04"]="darkflame-universe-linux" + ["build-macos-15-intel"]="darkflame-universe-macos" + ) + cd artifacts + for dir in */; do + name="${dir%/}" + out="${platform_map[$name]:-$name}" + zip -r "../${out}.zip" "$dir" + done + cd .. + ls -lh *.zip + + - name: Create GitHub Release + uses: ncipollo/release-action@v1 + with: + tag: ${{ github.event.workflow_run.head_branch }} + name: ${{ github.event.workflow_run.head_branch }} + bodyFile: CHANGES.md + artifacts: "*.zip" + artifactContentType: application/zip + token: ${{ secrets.GITHUB_TOKEN }} + prerelease: false + draft: false diff --git a/.gitignore b/.gitignore index d7af5d1f..b1a336af 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,5 @@ docker-compose.override.yml # CMake scripts !cmake/* !cmake/toolchains/* +.mcp.json +.claude/ diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..18b2597e --- /dev/null +++ b/cliff.toml @@ -0,0 +1,35 @@ +[changelog] +header = "" +body = """ +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | upper_first }} +{% for commit in commits %} +- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/commit/{{ commit.id }}))\ +{% if commit.github.username %} by [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }}){% endif %} +{% endfor %} +{% endfor %} +""" +footer = "" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^docs", group = "Documentation" }, + { message = "^chore", group = "Chores" }, + { message = "^test", group = "Testing" }, + { message = "^ci", group = "CI/CD" }, + { message = "^revert", group = "Reverts" }, +] +filter_commits = false +tag_pattern = "v[0-9].*" +skip_tags = "canary" +ignore_tags = "" +topo_order = false +sort_commits = "oldest"