diff --git a/.github/workflows/discord.yml b/.github/workflows/discord.yml new file mode 100644 index 0000000..59eb3c8 --- /dev/null +++ b/.github/workflows/discord.yml @@ -0,0 +1,73 @@ +name: Discord Notification + +on: + workflow_call: + inputs: + release_tag: + required: true + type: string + discord_title: + required: true + type: string + discord_color: + required: true + type: number + discord_footer: + required: true + type: string + secrets: + DISCORD_WEBHOOK: + required: true + GH_TOKEN: + required: true + +jobs: + notify-discord: + runs-on: ubuntu-latest + steps: + - name: Send Discord Notification + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + RELEASE_INFO=$(gh release view "${{ inputs.release_tag }}" -R ${{ github.repository }} --json name,url,body,author) + + RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name) + if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ inputs.release_tag }}"; fi + + RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url) + RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body) + + AUTHOR_NAME="Portabase" + AUTHOR_ICON="https://github.com/Portabase.png" + + PAYLOAD=$(jq -n \ + --arg title "$RELEASE_TITLE" \ + --arg description "$RELEASE_BODY" \ + --arg url "$RELEASE_URL" \ + --arg author "$AUTHOR_NAME" \ + --arg icon "$AUTHOR_ICON" \ + --arg discord_title "${{ inputs.discord_title }}" \ + --arg discord_footer "${{ inputs.discord_footer }}" \ + --argjson discord_color ${{ inputs.discord_color }} \ + '{ + content: $discord_title, + embeds: [{ + title: $title, + url: $url, + description: $description, + color: $discord_color, + author: { + name: $author, + icon_url: $icon + }, + footer: { + text: $discord_footer + } + }] + }' + ) + + curl -H "Content-Type: application/json" \ + -d "$PAYLOAD" \ + "$DISCORD_WEBHOOK" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 91066a6..a9547c6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,10 +3,13 @@ name: Docker Publish on: workflow_call: inputs: + version: + required: true + type: string image_name: required: false type: string - default: 'portabase/agent' + default: "portabase/agent" add_latest: required: false type: boolean @@ -14,11 +17,11 @@ on: target: required: false type: string - default: 'prod' + default: "prod" dockerfile: required: false type: string - default: './docker/Dockerfile' + default: "./docker/dockerfile/Dockerfile" secrets: DOCKER_USERNAME: required: true @@ -27,32 +30,32 @@ on: jobs: build: - name: Build and push Docker images - runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }} + name: Build architectures + runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }} strategy: fail-fast: false matrix: - platform: [linux/amd64, linux/arm64] + platform: [ linux/amd64, linux/arm64 ] steps: - name: Checkout uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker - uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Set image tag - id: set-tags + - name: Prepare Image Tags + id: prep run: | - REF_NAME=${GITHUB_REF#refs/tags/} - if [ "${{ matrix.platform }}" = "linux/amd64" ]; then - IMAGE="${{ inputs.image_name }}:$REF_NAME-amd64" - else - IMAGE="${{ inputs.image_name }}:$REF_NAME-arm64" - fi - echo "image=$IMAGE" >> $GITHUB_OUTPUT + ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} + echo "image=${{ inputs.image_name }}:${{inputs.version}}-$ARCH" >> $GITHUB_OUTPUT + echo "safe_platform=${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || 'linux-arm64' }}" >> $GITHUB_OUTPUT + - name: Build and push image uses: docker/build-push-action@v6 @@ -61,57 +64,73 @@ jobs: file: ${{ inputs.dockerfile }} platforms: ${{ matrix.platform }} push: true - tags: ${{ steps.set-tags.outputs.image }} + tags: ${{ steps.prep.outputs.image }} target: ${{ inputs.target }} + cache-from: type=gha,scope=build-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} - - name: Prepare artifact name - id: artifact - run: | - platform=${{ matrix.platform }} - echo "safe_platform=${platform//\//-}" >> $GITHUB_OUTPUT - echo "${{ steps.set-tags.outputs.image }}" > image.txt + - name: Save image name for manifest + run: echo "${{ steps.prep.outputs.image }}" > image.txt - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + - uses: actions/upload-artifact@v4 with: - name: image-${{ steps.artifact.outputs.safe_platform }} + name: image-${{ steps.prep.outputs.safe_platform }} path: image.txt - if-no-files-found: warn - compression-level: 6 - overwrite: false - include-hidden-files: false + retention-days: 1 create-manifest: - name: Create multi-arch Docker manifest + name: Create multi-arch manifest runs-on: ubuntu-latest needs: build steps: - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + - uses: actions/download-artifact@v4 with: name: image-linux-amd64 path: /tmp/digests/amd64 - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + - uses: actions/download-artifact@v4 with: name: image-linux-arm64 path: /tmp/digests/arm64 - name: Login to Docker - uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + - name: Generate semantic tags + id: tags + run: | + VERSION="${{ inputs.version }}" + IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" + + echo "VERSION_TAG=$VERSION" >> $GITHUB_OUTPUT + if [ -n "$MINOR" ]; then + echo "MINOR_TAG=$MAJOR.$MINOR" >> $GITHUB_OUTPUT + fi + if [ -n "$MAJOR" ]; then + echo "MAJOR_TAG=$MAJOR" >> $GITHUB_OUTPUT + fi + if [ "${{ inputs.add_latest }}" = "true" ]; then + echo "LATEST_TAG=latest" >> $GITHUB_OUTPUT + fi + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.image_name }} + tags: | + type=raw,value=${{ steps.tags.outputs.VERSION_TAG }} + type=raw,value=${{ steps.tags.outputs.MINOR_TAG }} + type=raw,value=${{ steps.tags.outputs.MAJOR_TAG }} + type=raw,value=${{ steps.tags.outputs.LATEST_TAG }} + - name: Create and push manifest list working-directory: /tmp/digests run: | DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)" - REF_NAME=${GITHUB_REF#refs/tags/} - MANIFEST_IMAGE="${{ inputs.image_name }}:$REF_NAME" - - docker buildx imagetools create $DOCKER_IMAGES -t $MANIFEST_IMAGE - docker buildx imagetools inspect $MANIFEST_IMAGE - - if [ "${{ inputs.add_latest }}" = "true" ]; then - docker buildx imagetools create $DOCKER_IMAGES -t ${{ inputs.image_name }}:latest - docker buildx imagetools inspect ${{ inputs.image_name }}:latest - fi \ No newline at end of file + TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") + echo $TAG_ARGS + docker buildx imagetools create $TAG_ARGS $DOCKER_IMAGES diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml deleted file mode 100644 index 1186d2d..0000000 --- a/.github/workflows/github.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: GitHub Release - -on: - workflow_call: - inputs: - prerelease: - required: false - type: boolean - default: false - make_latest: - required: false - type: boolean - default: false - discord_title: - required: true - type: string - discord_color: - required: true - type: number - discord_footer: - required: true - type: string - secrets: - DISCORD_WEBHOOK: - required: true - GH_TOKEN: - required: true - -jobs: - create-release: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Check out the repo - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build Changelog - id: build_changelog - uses: mikepenz/release-changelog-builder-action@v5 - with: - mode: "COMMIT" - configurationJson: | - { - "template": "#{{CHANGELOG}}", - "categories": [ - { - "title": "## Feature", - "labels": ["feat", "feature"] - }, - { - "title": "## Fix", - "labels": ["fix", "bug"] - }, - { - "title": "## Other", - "labels": [] - } - ], - "label_extractor": [ - { - "pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)", - "on_property": "title", - "target": "$1" - } - ] - } - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - generate_release_notes: false - body: ${{ steps.build_changelog.outputs.changelog }} - prerelease: ${{ inputs.prerelease }} - make_latest: ${{ inputs.make_latest }} - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - - name: Send Discord Notification - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} - run: | - RELEASE_INFO=$(gh release view "${{ github.ref_name }}" -R ${{ github.repository }} --json name,url,body,author) - - RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name) - if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ github.ref_name }}"; fi - - RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url) - RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body) - - AUTHOR_NAME="Portabase" - AUTHOR_ICON="https://github.com/Portabase.png" - - PAYLOAD=$(jq -n \ - --arg title "$RELEASE_TITLE" \ - --arg description "$RELEASE_BODY" \ - --arg url "$RELEASE_URL" \ - --arg author "$AUTHOR_NAME" \ - --arg icon "$AUTHOR_ICON" \ - --arg discord_title "${{ inputs.discord_title }}" \ - --arg discord_footer "${{ inputs.discord_footer }}" \ - --argjson discord_color ${{ inputs.discord_color }} \ - '{ - content: $discord_title, - embeds: [{ - title: $title, - url: $url, - description: $description, - color: $discord_color, - author: { - name: $author, - icon_url: $icon - }, - footer: { - text: $discord_footer - } - }] - }' - ) - - curl -H "Content-Type: application/json" \ - -d "$PAYLOAD" \ - "$DISCORD_WEBHOOK" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b5c83b..a770138 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,32 +1,132 @@ -name: Publish Docker image for release +name: Auto Release & Publish on: - push: - tags: - - '*.*.*' - - '!*-*' + pull_request: + types: [ closed ] + branches: + - main permissions: contents: write packages: write jobs: - docker_publish: + + check-skip: + runs-on: ubuntu-latest + outputs: + skip: ${{ steps.set-skip.outputs.skip }} + steps: + - name: Determine if release should be skipped + id: set-skip + run: | + TITLE="${{ github.event.pull_request.title }}" + echo "PR title: $TITLE" + + if [[ "$TITLE" == *"[skip-release]"* ]]; then + echo "PR title contains [skip-release], skipping release jobs." + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + create-release: + needs: check-skip + if: ${{ needs.check-skip.outputs.skip == 'false' }} + runs-on: ubuntu-latest + outputs: + draft_tag: ${{ steps.release_step.outputs.draft_tag }} + version: ${{ steps.release_step.outputs.version }} + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + ref: main + + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: "lts/*" + cache: "pnpm" + + - name: Install release-it globally + run: npm install -g release-it + + - run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + + - name: Run release-it + id: release_step + run: | + git pull origin main + + VERSION=$(release-it --ci --release-version) + echo $VERSION + echo "version=$VERSION" >> $GITHUB_OUTPUT + + OUTPUT=$(release-it --ci) + echo "$OUTPUT" + + DRAFT_TAG=$(echo "$OUTPUT" | grep -oE 'untagged-[a-z0-9]+') + echo $DRAFT_TAG + echo "draft_tag=$DRAFT_TAG" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + + + publish-docker: + needs: create-release + if: ${{ needs.create-release.result == 'success' }} uses: ./.github/workflows/docker.yml with: + version: ${{ needs.create-release.outputs.version }} add_latest: true secrets: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - github_release: - needs: docker_publish - uses: ./.github/workflows/github.yml + + finalize-release: + needs: + - create-release + - publish-docker + runs-on: ubuntu-latest + outputs: + release_tag: ${{ steps.publish_release_step.outputs.release_tag }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Publish GitHub Release + id: publish_release_step + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + OUTPUT=$(gh release edit ${{ needs.create-release.outputs.draft_tag }} --draft=false) + echo "$OUTPUT" + RELEASE_TAG=$(echo "$OUTPUT" | sed -E 's|.*/releases/tag/||') + echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT + + notify-discord: + needs: + - publish-docker + - create-release + - finalize-release + uses: ./.github/workflows/discord.yml with: - prerelease: false - make_latest: true + release_tag: ${{ needs.create-release.outputs.version }} discord_title: "||@release-agent|| New release published" - discord_color: 5814783 + discord_color: 3066993 discord_footer: "Portabase" secrets: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.release-it.json b/.release-it.json new file mode 100644 index 0000000..522fba1 --- /dev/null +++ b/.release-it.json @@ -0,0 +1,64 @@ +{ + "github": { + "release": true, + "draft": true, + "tokenRef": "GITHUB_TOKEN" + }, + "git": { + "commit": true, + "commitMessage": "chore: release ${version}", + "requireCleanWorkingDir": true, + "tag": true, + "tagName": "${version}", + "push": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": { + "name": "conventionalcommits", + "types": [ + { + "type": "feat", + "section": "✨ Features" + }, + { + "type": "fix", + "section": "🐛 Bug Fixes" + }, + { + "type": "perf", + "section": "⚡️ Performance Improvements" + }, + { + "type": "revert", + "section": "⏪️ Reverts" + }, + { + "type": "docs", + "section": "📝 Documentation", + "hidden": true + }, + { + "type": "chore", + "section": "🔧 Chores", + "hidden": true + } + ] + } + }, + "@release-it/bumper": { + "out": [ + { + "file": "Cargo.toml", + "path": "package.version", + "type": "toml" + }, + { + "file": "CITATION.cff", + "path": "version", + "type": "text/yaml" + } + ] + } + } +} diff --git a/release b/release deleted file mode 100755 index 4fb10db..0000000 --- a/release +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash - -set -e - -if [ -z "$1" ]; then - echo "Usage: ./release " - echo "Example: ./release v1.0.0" - exit 1 -fi - -VERSION=$1 -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) - -if [ "$CURRENT_BRANCH" = "main" ]; then - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: On 'main' branch, only release tags (X.Y.Z) are allowed." - exit 1 - fi -else - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then - echo "Error: On branch '$CURRENT_BRANCH', only RC tags matching X.Y.Z-rc.W are allowed (e.g., 1.0.0-rc.1)." - exit 1 - fi -fi - -CLEAN_VERSION=${VERSION#v} -CURRENT_DATE=$(date +%Y-%m-%d) - -echo "Preparing release $VERSION..." - - -# package.json -if [ -f package.json ]; then - echo "Updating package.json..." - if sed --version >/dev/null 2>&1; then - sed -i "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json - else - sed -i '' "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json - fi -fi - -# pyproject.toml -if [ -f pyproject.toml ]; then - echo "Updating pyproject.toml..." - if sed --version >/dev/null 2>&1; then - sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml - else - sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml - fi -fi - -# Cargo.toml -if [ -f Cargo.toml ]; then - echo "Updating Cargo.toml..." - if sed --version >/dev/null 2>&1; then - sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml - else - sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml - fi -fi - -# CITATION.cff -if [ -f CITATION.cff ]; then - echo "Updating CITATION.cff..." - if sed --version >/dev/null 2>&1; then - sed -i "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff - sed -i "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff - else - sed -i '' "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff - sed -i '' "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff - fi -fi - - - - - -git add . - -if ! git diff-index --quiet HEAD --; then - echo "Committing changes..." - git commit -m "chore(release): $VERSION" -else - echo "No changes to commit. Proceeding to tag..." -fi - -if git rev-parse "$VERSION" >/dev/null 2>&1; then - echo "Tag $VERSION already exists. Aborting." - exit 1 -fi - -echo "Creating tag $VERSION..." -git tag -a "$VERSION" -m "Release $VERSION" - -echo "Pushing changes and tags to remote..." -git push -git push origin "$VERSION" - -echo "Successfully released $VERSION!" \ No newline at end of file