diff --git a/.github/actions/validate-version/action.yml b/.github/actions/validate-version/action.yml new file mode 100644 index 0000000..4200b0e --- /dev/null +++ b/.github/actions/validate-version/action.yml @@ -0,0 +1,72 @@ +name: Validate Version +description: Extract and validate version from VERSION file 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 VERSION file + value: ${{ steps.extract.outputs.version }} + +runs: + using: composite + steps: + - name: Extract version from VERSION file + id: extract + shell: bash + run: | + VERSION=$(cat VERSION | tr -d '\n') + 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 index b7c930e..d0aeb82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,10 @@ on: branches: - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test-macos: name: Test on macOS @@ -15,6 +19,16 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache Swift packages + uses: actions/cache@v4 + with: + path: .build + key: spm-${{ runner.os }}-${{ hashFiles('Package.swift') }} + restore-keys: | + spm-${{ runner.os }}- - name: Check Swift version run: swift --version @@ -28,6 +42,17 @@ jobs: - name: Build CLI run: swift build -c release + - name: Validate version + id: version-check + uses: ./.github/actions/validate-version + with: + check-tag-exists: 'true' + continue-on-error: true + + - name: Version validation warning + if: steps.version-check.outcome == 'failure' + run: echo "::warning::Version validation failed. Remember to bump the version before creating a release." + test-linux: name: Test on Linux runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b933d7..4aed731 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,11 @@ name: Release on: - push: - tags: - - 'v*' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build-macos: @@ -12,19 +14,31 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache Swift packages + uses: actions/cache@v4 + with: + path: .build + key: spm-${{ runner.os }}-${{ hashFiles('Package.swift') }} + restore-keys: | + spm-${{ runner.os }}- - name: Check Swift version run: swift --version - - name: Get version from tag - id: get_version - run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + - name: Validate version + id: version + uses: ./.github/actions/validate-version + with: + check-tag-exists: 'true' - name: Run tests run: swift test - name: Build release binary - run: ./scripts/build-release.sh ${{ steps.get_version.outputs.VERSION }} + run: ./scripts/build-release.sh ${{ steps.version.outputs.version }} - name: Upload artifacts uses: actions/upload-artifact@v4 @@ -43,6 +57,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Download macOS artifact uses: actions/download-artifact@v4 @@ -50,29 +66,37 @@ jobs: name: macos-universal path: release/ - - name: Get version from tag - id: get_version - run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + - name: Validate version + id: version + uses: ./.github/actions/validate-version + with: + check-tag-exists: 'true' - name: Generate release notes - id: release_notes run: | - cat > release_notes.md << 'EOF' + # Get previous tag for changelog + PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' | head -n1) + + # Generate changelog + echo "## What's Changed" > release_notes.md + echo "" >> release_notes.md + if [ -z "$PREV_TAG" ]; then + git log --pretty=format:"- %s" HEAD >> release_notes.md + else + git log --pretty=format:"- %s" ${PREV_TAG}..HEAD >> release_notes.md + fi + echo "" >> release_notes.md + echo "" >> release_notes.md + + # Append static content + cat >> release_notes.md << 'EOF' ## Installation ### macOS (Universal Binary - x86_64 + ARM64) Download and install: ```bash - curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/ejson-${{ steps.get_version.outputs.VERSION }}-macos-universal.tar.gz | tar xz - sudo mv ejson /usr/local/bin/ - ejson --version - ``` - - Or with wget: - ```bash - wget https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/ejson-${{ steps.get_version.outputs.VERSION }}-macos-universal.tar.gz - tar xzf ejson-${{ steps.get_version.outputs.VERSION }}-macos-universal.tar.gz + curl -L https://github.com/${{ github.repository }}/releases/latest/download/ejson-macos-universal.tar.gz | tar xz sudo mv ejson /usr/local/bin/ ejson --version ``` @@ -80,34 +104,8 @@ jobs: ### Verify Checksum ```bash - # Download checksum file - curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/ejson-${{ steps.get_version.outputs.VERSION }}-macos-universal.tar.gz.sha256 -o ejson.sha256 - - # Verify (macOS) + curl -L https://github.com/${{ github.repository }}/releases/latest/download/ejson-macos-universal.tar.gz.sha256 -o ejson.sha256 shasum -a 256 -c ejson.sha256 - - # Verify (Linux) - sha256sum -c ejson.sha256 - ``` - - ## Features - - - 🔐 NaCl Box encryption compatible with Shopify EJSON - - 🔄 Full compatibility with Go EJSON implementation - - ⚡ Fast and native Swift implementation - - 📦 Universal macOS binary (works on both Intel and Apple Silicon) - - ## Usage - - ```bash - # Generate a keypair - ejson keygen - - # Encrypt a file - ejson encrypt secrets.json - - # Decrypt a file - ejson decrypt secrets.json ``` For more information, see the [README](https://github.com/${{ github.repository }}/blob/main/README.md). @@ -117,6 +115,8 @@ jobs: uses: diogot/gh-actions-workflows/actions/create-release@main with: token: ${{ secrets.GITHUB_TOKEN }} + tag: v${{ steps.version.outputs.version }} + title: v${{ steps.version.outputs.version }} body-file: release_notes.md files: | release/*.tar.gz diff --git a/.gitignore b/.gitignore index 8597743..58a9cf2 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ Package.resolved # .swiftpm .build/ +.swiftpm/ release/ # CocoaPods diff --git a/CLAUDE.md b/CLAUDE.md index 85bb4fd..6817e34 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,10 @@ **Reference Implementation:** https://github.com/Shopify/ejson +## Before Starting a New Feature + +**Remember to bump the version!** Before starting work on a new feature or bug fix, update the `VERSION` file at the project root with the new version number. This ensures the release workflow can create a new release when the changes are merged. + ## Purpose Provide a native Swift library that can: diff --git a/Package.swift b/Package.swift index e8d3340..564f28c 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/jedisct1/swift-sodium.git", from: "0.9.1") + .package(url: "https://github.com/jedisct1/swift-sodium.git", from: "0.10.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -33,9 +33,16 @@ let package = Package( ]), .executableTarget( name: "ejson", - dependencies: ["EJSONKit"]), + dependencies: ["EJSONKit"], + plugins: [ + .plugin(name: "BuildVersionPlugin") + ]), .testTarget( name: "EJSONKitTests", dependencies: ["EJSONKit"]), + .plugin( + name: "BuildVersionPlugin", + capability: .buildTool() + ), ] ) diff --git a/Plugins/BuildVersionPlugin/plugin.swift b/Plugins/BuildVersionPlugin/plugin.swift new file mode 100644 index 0000000..2ad50ba --- /dev/null +++ b/Plugins/BuildVersionPlugin/plugin.swift @@ -0,0 +1,31 @@ +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 [ + .buildCommand( + 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 + """ + ], + inputFiles: [versionFile], + outputFiles: [outputFile] + ) + ] + } +} diff --git a/README.md b/README.md index 31b12be..837efdb 100644 --- a/README.md +++ b/README.md @@ -292,17 +292,16 @@ Download the latest release for your platform from [GitHub Releases](https://git ```bash # Download and install the latest version -VERSION="1.0.0" # Replace with latest version -curl -L "https://github.com/diogot/swift-ejson/releases/download/v${VERSION}/ejson-${VERSION}-macos-universal.tar.gz" | tar xz +curl -L https://github.com/diogot/swift-ejson/releases/latest/download/ejson-macos-universal.tar.gz | tar xz sudo mv ejson /usr/local/bin/ -ejson help +ejson --version ``` **Verify the checksum:** ```bash # Download checksum -curl -L "https://github.com/diogot/swift-ejson/releases/download/v${VERSION}/ejson-${VERSION}-macos-universal.tar.gz.sha256" -o ejson.sha256 +curl -L https://github.com/diogot/swift-ejson/releases/latest/download/ejson-macos-universal.tar.gz.sha256 -o ejson.sha256 # Verify shasum -a 256 -c ejson.sha256 @@ -341,6 +340,7 @@ Commands: Global Options: -keydir Path to keydir (default: /opt/ejson/keys or $EJSON_KEYDIR) + --version, -v Print version and exit Keygen Options: -w Write private key to keydir and print only public key @@ -424,7 +424,7 @@ swift test ## Dependencies -- [swift-sodium](https://github.com/jedisct1/swift-sodium) (v0.9.1+) - Provides NaCl cryptography primitives with bundled libsodium +- [swift-sodium](https://github.com/jedisct1/swift-sodium) (v0.10.0+) - Provides NaCl cryptography primitives with bundled libsodium ## Requirements @@ -441,6 +441,14 @@ Contributions are welcome! Please ensure: - Code follows Swift conventions - Changes maintain compatibility with Go EJSON +### Version Management + +The version is managed via the `VERSION` file at the project root. Before creating a release: + +1. Update the `VERSION` file with the new version number +2. Commit and push to `main` +3. Trigger the Release workflow manually from GitHub Actions + ## License MIT License - See LICENSE file for details diff --git a/RELEASING.md b/RELEASING.md index 646cf53..d5f2bed 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,54 +2,57 @@ This document describes how to create a new release of swift-ejson. +## Version Management + +The version is managed via the `VERSION` file at the project root. This file is the single source of truth for the version number and is read at build time by the `BuildVersionPlugin` to inject the version into the CLI binary. + ## Prerequisites - Write access to the repository - All tests passing on main branch -- Updated CHANGELOG (if applicable) ## Release Steps -### 1. Prepare the Release +### 1. Update the VERSION file -Ensure all changes for the release are merged to the main branch: +Edit the `VERSION` file to contain the new version number: ```bash -git checkout main -git pull origin main +echo "1.2.3" > VERSION ``` -### 2. Update Version References - -Update version references in documentation if needed: - -- README.md (example version numbers in CLI installation) -- Package.swift (if you maintain a version constant) - -### 3. Create and Push a Tag +Follow [Semantic Versioning](https://semver.org/): +- **MAJOR** version: Incompatible API changes +- **MINOR** version: New functionality (backwards compatible) +- **PATCH** version: Bug fixes (backwards compatible) -Create a version tag following semantic versioning (vMAJOR.MINOR.PATCH): +### 2. Commit and Push ```bash -# Create an annotated tag -git tag -a v1.0.0 -m "Release version 1.0.0" - -# Push the tag to GitHub -git push origin v1.0.0 +git add VERSION +git commit -m "Bump version to 1.2.3" +git push origin main ``` -### 4. Automated Build Process +### 3. Trigger the Release Workflow + +1. Go to the [Actions tab](https://github.com/diogot/swift-ejson/actions) +2. Select the "Release" workflow from the left sidebar +3. Click "Run workflow" button +4. Select the `main` branch +5. Click "Run workflow" -Once you push the tag, GitHub Actions will automatically: +### 4. Automated Build Process -1. **Run Tests** - Ensure all tests pass on macOS and Linux -2. **Build macOS Binary** - Create a universal binary (x86_64 + ARM64) -3. **Create Archive** - Package the binary as `.tar.gz` -4. **Calculate Checksums** - Generate SHA256 checksums -5. **Create GitHub Release** - Publish release with all artifacts -6. **Upload Assets** - Attach binaries and checksums +The Release workflow will automatically: -You can monitor the progress at: `https://github.com/diogot/swift-ejson/actions` +1. **Validate version** - Ensure VERSION > latest tag and tag doesn't exist +2. **Run tests** - Ensure all tests pass +3. **Build macOS binary** - Create a universal binary (x86_64 + ARM64) +4. **Create archive** - Package the binary as `.tar.gz` +5. **Calculate checksums** - Generate SHA256 checksums +6. **Create GitHub Release** - Publish release with tag `v{VERSION}` +7. **Upload assets** - Attach binaries and checksums ### 5. Verify the Release @@ -57,6 +60,7 @@ After the workflow completes: 1. Go to https://github.com/diogot/swift-ejson/releases 2. Verify the release was created with: + - Correct version tag (e.g., `v1.2.3`) - Release notes - Binary archive (`.tar.gz`) - Checksum file (`.sha256`) @@ -66,9 +70,9 @@ After the workflow completes: Download and test the binary: ```bash -VERSION="1.0.0" +VERSION="1.2.3" curl -L "https://github.com/diogot/swift-ejson/releases/download/v${VERSION}/ejson-${VERSION}-macos-universal.tar.gz" | tar xz -./ejson help +./ejson --version ./ejson keygen ``` @@ -79,21 +83,14 @@ curl -L "https://github.com/diogot/swift-ejson/releases/download/v${VERSION}/ejs shasum -a 256 -c ejson.sha256 ``` -## Versioning Guidelines - -We follow [Semantic Versioning](https://semver.org/): +## Troubleshooting -- **MAJOR** version: Incompatible API changes -- **MINOR** version: New functionality (backwards compatible) -- **PATCH** version: Bug fixes (backwards compatible) +### Version Validation Fails -Examples: -- `v1.0.0` - Initial stable release -- `v1.1.0` - Add new features -- `v1.0.1` - Bug fixes -- `v2.0.0` - Breaking changes +If the release workflow fails with version validation errors: -## Troubleshooting +1. **"Tag already exists"** - The VERSION matches an existing release. Bump the version. +2. **"Version must be greater than latest tag"** - Ensure VERSION > latest tag (e.g., 1.2.3 > 1.2.2) ### Build Fails @@ -101,17 +98,8 @@ If the GitHub Actions workflow fails: 1. Check the Actions tab for error details 2. Fix issues in a new commit/PR -3. Delete the tag: `git tag -d v1.0.0 && git push origin :refs/tags/v1.0.0` -4. Create a new tag after fixes are merged - -### Release Already Exists - -If you need to update a release: - -1. Go to the Releases page -2. Edit the release -3. Delete old assets if needed -4. Manually upload new assets (or delete the release and re-run workflow) +3. Merge to main +4. Re-run the Release workflow ## Manual Release (Fallback) @@ -124,27 +112,17 @@ If automated builds fail, you can create a release manually: rm -rf .build release # Run build script -./scripts/build-release.sh 1.0.0 +./scripts/build-release.sh 1.2.3 # Verify the binary -./release/ejson help +./release/ejson --version ``` ### Create Release on GitHub -1. Go to https://github.com/diogot/swift-ejson/releases/new -2. Choose the tag (v1.0.0) -3. Fill in the release title and description -4. Upload the files from `release/` directory: - - `ejson-1.0.0-macos-universal.tar.gz` - - `ejson-1.0.0-macos-universal.tar.gz.sha256` -5. Click "Publish release" - -## Post-Release - -After releasing: - -1. Announce the release (Twitter, forums, etc.) -2. Update documentation sites if applicable -3. Close related issues/PRs -4. Plan next release milestone +1. Create and push a tag: `git tag v1.2.3 && git push origin v1.2.3` +2. Go to https://github.com/diogot/swift-ejson/releases/new +3. Choose the tag (v1.2.3) +4. Fill in the release title and description +5. Upload the files from `release/` directory +6. Click "Publish release" diff --git a/Sources/ejson/main.swift b/Sources/ejson/main.swift index 9eacda2..dd992bd 100644 --- a/Sources/ejson/main.swift +++ b/Sources/ejson/main.swift @@ -260,6 +260,8 @@ func main() { decryptCommand(args: commandArgs) case "-h", "--help", "help": printUsage() + case "-v", "--version", "version": + print(GeneratedVersion.version) default: exitWithError("Unknown command: \(command)\nRun 'ejson help' for usage information") } diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..23aa839 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.2.2 diff --git a/scripts/build-release.sh b/scripts/build-release.sh index cca500b..17db086 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -72,14 +72,24 @@ echo "Verifying binary..." echo "Creating archive: $ARCHIVE_NAME" cd "$RELEASE_DIR" tar -czf "$ARCHIVE_NAME" "$PRODUCT_NAME" + +# Create latest archive (without version) for GitHub latest release URL +if [[ "$PLATFORM" == "macos" ]]; then + LATEST_ARCHIVE="${PRODUCT_NAME}-macos-universal.tar.gz" +else + LATEST_ARCHIVE="${PRODUCT_NAME}-linux-$(uname -m).tar.gz" +fi +cp "$ARCHIVE_NAME" "$LATEST_ARCHIVE" cd .. # Calculate checksums echo "Calculating checksums..." if command -v sha256sum &> /dev/null; then sha256sum "$RELEASE_DIR/$ARCHIVE_NAME" > "$RELEASE_DIR/$ARCHIVE_NAME.sha256" + sha256sum "$RELEASE_DIR/$LATEST_ARCHIVE" > "$RELEASE_DIR/$LATEST_ARCHIVE.sha256" elif command -v shasum &> /dev/null; then shasum -a 256 "$RELEASE_DIR/$ARCHIVE_NAME" > "$RELEASE_DIR/$ARCHIVE_NAME.sha256" + shasum -a 256 "$RELEASE_DIR/$LATEST_ARCHIVE" > "$RELEASE_DIR/$LATEST_ARCHIVE.sha256" fi echo ""