diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b6e48dc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + rebase-strategy: "auto" diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..05ba5c5 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,86 @@ +name: Auto release + +on: + pull_request: + types: [closed] + branches: + - main + +permissions: + contents: write + +jobs: + release: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: Determine version bump + id: bump + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + if [[ "$PR_TITLE" =~ ^[a-z]+(\(.+\))?\!: ]]; then + echo "type=major" >> "$GITHUB_OUTPUT" + elif [[ "$PR_TITLE" =~ ^feat(\(.+\))?: ]]; then + echo "type=minor" >> "$GITHUB_OUTPUT" + elif [[ "$PR_TITLE" =~ ^(fix|perf|refactor)(\(.+\))?\!?: ]]; then + echo "type=patch" >> "$GITHUB_OUTPUT" + else + echo "Skipping release for: $PR_TITLE" + echo "type=skip" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/checkout@v6.0.2 + if: steps.bump.outputs.type != 'skip' + with: + fetch-depth: 0 + + - name: Calculate next version + if: steps.bump.outputs.type != 'skip' + id: version + env: + BUMP_TYPE: ${{ steps.bump.outputs.type }} + run: | + latest=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1) + if [ -z "$latest" ]; then + latest="v0.0.0" + fi + version="${latest#v}" + IFS='.' read -r major minor patch <<< "$version" + + case "$BUMP_TYPE" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + esac + + next="v${major}.${minor}.${patch}" + echo "tag=$next" >> "$GITHUB_OUTPUT" + echo "major=v${major}" >> "$GITHUB_OUTPUT" + echo "Bumping ${BUMP_TYPE}: $latest -> $next" + + - name: Create GitHub release + if: steps.bump.outputs.type != 'skip' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NEW_TAG: ${{ steps.version.outputs.tag }} + run: | + gh release create "$NEW_TAG" \ + --title "$NEW_TAG" \ + --generate-notes \ + --target main + + # Major bump: creates the new simple tag (e.g. v2) at the new commit; + # the previous major tag (v1) stays at its last commit so existing + # pins keep receiving only v1.x updates. + # Minor/patch bump: moves the existing major tag (v1) to the new commit. + - name: Update major version tag + if: steps.bump.outputs.type != 'skip' + env: + NEW_TAG: ${{ steps.version.outputs.tag }} + MAJOR_TAG: ${{ steps.version.outputs.major }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -fa "$MAJOR_TAG" -m "$NEW_TAG" + git push --force origin "$MAJOR_TAG" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..1170654 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,18 @@ +name: Auto-merge Dependabot PRs + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Enable auto-merge + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh pr merge --auto --rebase "$PR_URL" diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..6a9c1e7 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,29 @@ +name: Validate PR title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + branches: + - main + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Check conventional commit format + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + pattern='^(feat|fix|docs|chore|refactor|test|ci|perf|build|style)(\(.+\))?\!?: .+' + if [[ ! "$PR_TITLE" =~ $pattern ]]; then + echo "::error::PR title must follow conventional commit format:" + echo "::error:: [optional scope][!]: " + echo "::error::Allowed types: feat, fix, docs, chore, refactor, test, ci, perf, build, style" + echo "::error::Add '!' before ':' for breaking changes (e.g., 'feat!: redesign API')" + echo "::error::Got: '$PR_TITLE'" + exit 1 + fi + echo "PR title is valid: $PR_TITLE" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a5535d8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,102 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + - name: Run actionlint + uses: reviewdog/action-actionlint@v1 + with: + fail_level: error + + smoke: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v6.0.2 + + # Install pacto directly instead of using the action's `setup` command: + # the upstream install script makes unauthenticated api.github.com calls + # that get rate-limited on shared macOS runner IPs. We resolve "latest" + # ourselves with an authenticated request (5000/h token limit) so the + # smoke test always runs against the newest pacto release. + - name: Install pacto (latest) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + tag=$(curl -fsSL \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + https://api.github.com/repos/TrianaLab/pacto/releases/latest \ + | grep -E '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + if [ -z "$tag" ]; then + echo "::error::Could not resolve latest pacto release" + exit 1 + fi + case "$(uname -m)" in + x86_64|amd64) arch=amd64 ;; + aarch64|arm64) arch=arm64 ;; + *) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;; + esac + os="$(uname | tr '[:upper:]' '[:lower:]')" + url="https://github.com/TrianaLab/pacto/releases/download/${tag}/pacto_${os}_${arch}" + echo "Installing pacto ${tag} from ${url}" + curl -fsSL -o /tmp/pacto "$url" + chmod +x /tmp/pacto + sudo mv /tmp/pacto /usr/local/bin/pacto + pacto version + + - name: Create fixture contract + run: | + mkdir -p contract + cat > contract/pacto.yaml <<'EOF' + pactoVersion: "1.0" + service: + name: smoke-test + version: 1.0.0 + EOF + + - name: Validate contract + uses: ./ + with: + command: validate + path: contract + + - name: Generate doc + uses: ./ + with: + command: doc + path: contract + add-to-summary: 'false' + + - name: Diff contract against itself + id: diff + uses: ./ + with: + command: diff + old: contract + new: contract + fail-on-breaking: 'true' + + - name: Verify no breaking changes detected + run: | + if [ "${{ steps.diff.outputs.has-breaking-changes }}" != "false" ]; then + echo "::error::Expected has-breaking-changes=false, got '${{ steps.diff.outputs.has-breaking-changes }}'" + exit 1 + fi