From a238a34a617af83d6013d78790a08ebae32c0741 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Wed, 21 Jan 2026 17:52:07 -0300 Subject: [PATCH 1/2] Add GitHub Actions CI/CD workflows - CI workflow for PR testing (build + test) on dev and main branches - Version validation: fails on main PRs, warns on dev PRs - Release workflow: builds and publishes on push to main - Shared validate-version composite action for DRY validation logic Co-Authored-By: Claude Opus 4.5 --- .github/actions/validate-version/action.yml | 72 +++++++++++++++++++++ .github/workflows/ci.yml | 41 ++++++++++++ .github/workflows/release.yml | 33 ++++++++++ 3 files changed, 146 insertions(+) create mode 100644 .github/actions/validate-version/action.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/actions/validate-version/action.yml b/.github/actions/validate-version/action.yml new file mode 100644 index 0000000..bdab67b --- /dev/null +++ b/.github/actions/validate-version/action.yml @@ -0,0 +1,72 @@ +name: Validate Version +description: Extract and validate version from source code against latest git tag + +inputs: + check-tag-exists: + description: Fail if the tag already exists + required: false + default: 'false' + +outputs: + version: + description: The extracted version from source code + value: ${{ steps.extract.outputs.version }} + +runs: + using: composite + steps: + - name: Extract version from source + id: extract + shell: bash + run: | + VERSION=$(grep -o 'version: "[^"]*"' Sources/swift-outdated/SwiftOutdated.swift | sed 's/version: "\(.*\)"/\1/') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - name: Get latest tag + id: latest_tag + shell: bash + run: | + LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || echo "") + if [ -z "$LATEST_TAG" ]; then + echo "tag=" >> $GITHUB_OUTPUT + echo "No existing tags found" + else + echo "tag=${LATEST_TAG#v}" >> $GITHUB_OUTPUT + echo "Latest tag: $LATEST_TAG" + fi + + - name: Check if tag exists + if: inputs.check-tag-exists == 'true' + shell: bash + run: | + VERSION="${{ steps.extract.outputs.version }}" + if git rev-parse "v$VERSION" >/dev/null 2>&1; then + echo "Error: Tag v$VERSION already exists" + exit 1 + fi + + - name: Validate version bump + shell: bash + run: | + VERSION="${{ steps.extract.outputs.version }}" + LATEST="${{ steps.latest_tag.outputs.tag }}" + + echo "Current version: $VERSION" + echo "Latest tag: $LATEST" + + if [ -z "$LATEST" ]; then + echo "No existing tags. Version $VERSION is valid." + exit 0 + fi + + # Check if version is greater than latest tag using sort -V + HIGHEST=$(printf '%s\n%s' "$VERSION" "$LATEST" | sort -V | tail -n1) + + if [ "$HIGHEST" = "$VERSION" ] && [ "$VERSION" != "$LATEST" ]; then + echo "Version $VERSION is greater than $LATEST" + exit 0 + else + echo "Error: Version $VERSION must be greater than latest tag $LATEST" + exit 1 + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4c7ed16 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + pull_request: + branches: + - dev + - main + +jobs: + build-and-test: + name: Build and Test + runs-on: macos-26 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build + run: swift build + + - name: Run tests + run: swift test + + - name: Validate version (main) + if: github.base_ref == 'main' + uses: ./.github/actions/validate-version + with: + check-tag-exists: 'true' + + - name: Validate version (dev) + id: version-check + if: github.base_ref == 'dev' + uses: ./.github/actions/validate-version + with: + check-tag-exists: 'true' + continue-on-error: true + + - name: Version validation warning + if: github.base_ref == 'dev' && steps.version-check.outcome == 'failure' + run: echo "::warning::Version validation failed. Remember to bump the version before merging to main." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..20c07e5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + release: + name: Build and Release + runs-on: macos-26 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate version + id: version + uses: ./.github/actions/validate-version + with: + check-tag-exists: 'true' + + - name: Build release binary + run: swift build -c release + + - name: Create release + uses: diogot/gh-actions-workflows/actions/create-release@main + with: + tag: v${{ steps.version.outputs.version }} + title: v${{ steps.version.outputs.version }} + files: .build/release/swift-outdated + token: ${{ secrets.GITHUB_TOKEN }} From 2ccda135baf503b27e1ca84871ca378b9632b3b6 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Wed, 21 Jan 2026 21:36:47 -0300 Subject: [PATCH 2/2] Add BuildVersionPlugin for version injection - Add VERSION file as single source of truth for version - Create BuildVersionPlugin build tool plugin to generate version code - Update SwiftOutdated to use generated version constant - Update GitHub Actions to read version from VERSION file Co-Authored-By: Claude Opus 4.5 --- .github/actions/validate-version/action.yml | 8 +++--- Package.swift | 7 +++++ Plugins/BuildVersionPlugin/plugin.swift | 30 +++++++++++++++++++++ Sources/swift-outdated/SwiftOutdated.swift | 2 +- VERSION | 1 + 5 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 Plugins/BuildVersionPlugin/plugin.swift create mode 100644 VERSION diff --git a/.github/actions/validate-version/action.yml b/.github/actions/validate-version/action.yml index bdab67b..4200b0e 100644 --- a/.github/actions/validate-version/action.yml +++ b/.github/actions/validate-version/action.yml @@ -1,5 +1,5 @@ name: Validate Version -description: Extract and validate version from source code against latest git tag +description: Extract and validate version from VERSION file against latest git tag inputs: check-tag-exists: @@ -9,17 +9,17 @@ inputs: outputs: version: - description: The extracted version from source code + description: The extracted version from VERSION file value: ${{ steps.extract.outputs.version }} runs: using: composite steps: - - name: Extract version from source + - name: Extract version from VERSION file id: extract shell: bash run: | - VERSION=$(grep -o 'version: "[^"]*"' Sources/swift-outdated/SwiftOutdated.swift | sed 's/version: "\(.*\)"/\1/') + VERSION=$(cat VERSION | tr -d '\n') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Extracted version: $VERSION" diff --git a/Package.swift b/Package.swift index 61c9cf8..51aa15d 100644 --- a/Package.swift +++ b/Package.swift @@ -15,11 +15,18 @@ let package = Package( .package(url: "https://github.com/tuist/XcodeProj.git", from: "8.0.0") ], targets: [ + .plugin( + name: "BuildVersionPlugin", + capability: .buildTool() + ), .executableTarget( name: "swift-outdated", dependencies: [ "SwiftOutdatedCore", .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + plugins: [ + .plugin(name: "BuildVersionPlugin") ] ), .target( diff --git a/Plugins/BuildVersionPlugin/plugin.swift b/Plugins/BuildVersionPlugin/plugin.swift new file mode 100644 index 0000000..c995912 --- /dev/null +++ b/Plugins/BuildVersionPlugin/plugin.swift @@ -0,0 +1,30 @@ +import PackagePlugin +import Foundation + +@main +struct BuildVersionPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + let versionFile = context.package.directoryURL.appending(path: "VERSION") + let outputFile = context.pluginWorkDirectoryURL.appending(path: "GeneratedVersion.swift") + + return [ + .prebuildCommand( + displayName: "Generate version from VERSION file", + executable: URL(fileURLWithPath: "/bin/bash"), + arguments: [ + "-c", + """ + VERSION=$(cat "\(versionFile.path)" | tr -d '\\n') + cat > "\(outputFile.path)" << EOF + // Auto-generated by BuildVersionPlugin - do not edit + enum GeneratedVersion { + static let version = "$VERSION" + } + EOF + """ + ], + outputFilesDirectory: context.pluginWorkDirectoryURL + ) + ] + } +} diff --git a/Sources/swift-outdated/SwiftOutdated.swift b/Sources/swift-outdated/SwiftOutdated.swift index 475e038..1d710de 100644 --- a/Sources/swift-outdated/SwiftOutdated.swift +++ b/Sources/swift-outdated/SwiftOutdated.swift @@ -13,7 +13,7 @@ struct SwiftOutdated: AsyncParsableCommand { By default, searches for Package.resolved in the current directory or within Xcode project/workspace directories. """, - version: "1.0.0" + version: GeneratedVersion.version ) @Flag(name: .long, help: "Output results in JSON format") diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0