diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..7a89735 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,70 @@ +name: Linters + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + linter: + name: 'Frappe Linter' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: pip + - name: Install pre-commit + run: pip install pre-commit + - name: Run pre-commit on changed files + run: | + pre-commit run \ + --show-diff-on-failure \ + --color=always \ + --from-ref origin/${{ github.base_ref }} \ + --to-ref HEAD + + - name: Download Semgrep rules + run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + + - name: Run Semgrep rules + run: | + pip install semgrep + semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + deps-vulnerable-check: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install and run pip-audit + run: | + pip install pip-audit + cd ${GITHUB_WORKSPACE} + pip-audit --desc on . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c065d93 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,46 @@ +name: Generate Semantic Release + +on: + push: + branches: + - version-15 + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup dependencies + run: | + npm install @semantic-release/git @semantic-release/exec --no-save + + - name: Create Release + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} + GIT_AUTHOR_NAME: "Aakvatech Release Bot" + GIT_AUTHOR_EMAIL: "info@aakvatech.com" + GIT_COMMITTER_NAME: "Aakvatech Release Bot" + GIT_COMMITTER_EMAIL: "info@aakvatech.com" + run: npx semantic-release diff --git a/.github/workflows/tag-and-promote-from-pr-label.yml b/.github/workflows/tag-and-promote-from-pr-label.yml new file mode 100644 index 0000000..8b302ba --- /dev/null +++ b/.github/workflows/tag-and-promote-from-pr-label.yml @@ -0,0 +1,178 @@ +name: Tag and promote from PR label + +on: + pull_request_target: + types: + - closed + - labeled + +permissions: + contents: write + pull-requests: read + +jobs: + tag-and-promote: + if: > + github.event.pull_request.merged == true && + ( + github.event.action == 'closed' || + ( + github.event.action == 'labeled' && + startsWith(github.event.label.name, 'promote/') + ) + ) + runs-on: ubuntu-latest + + steps: + - name: Determine target branch from PR labels + id: target + uses: actions/github-script@v8 + with: + script: | + const labels = context.payload.pull_request.labels.map(label => label.name); + + const mapping = { + "promote/version-15": "version-15", + "promote/version-16": "version-16", + "promote/production": "production" + }; + + const matchedLabels = labels.filter(label => mapping[label]); + + if (matchedLabels.length === 0) { + core.info( + `No promote target label found. Skipping promotion. Add one of: ${Object.keys(mapping).join(", ")}` + ); + core.setOutput("should_promote", "false"); + return; + } + + if (matchedLabels.length > 1) { + core.setFailed( + `Multiple promote target labels found: ${matchedLabels.join(", ")}. Keep only one.` + ); + return; + } + + const matchedLabel = matchedLabels[0]; + + core.setOutput("should_promote", "true"); + core.setOutput("target_branch", mapping[matchedLabel]); + core.setOutput("matched_label", matchedLabel); + + - name: Checkout merged commit + if: steps.target.outputs.should_promote == 'true' + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + fetch-depth: 0 + + - name: Read version from servicems.__version__ + if: steps.target.outputs.should_promote == 'true' + id: version + shell: bash + run: | + VERSION=$(python - <<'PY' + import re + from pathlib import Path + + init_file = Path("servicems/__init__.py") + content = init_file.read_text() + + match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', content, re.M) + + if not match: + raise SystemExit("Could not find __version__ in servicems/__init__.py") + + print(match.group(1)) + PY + ) + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + + - name: Configure git user + if: steps.target.outputs.should_promote == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Create and push version tag + if: steps.target.outputs.should_promote == 'true' + shell: bash + run: | + git fetch --tags + + TAG="${{ steps.version.outputs.tag }}" + CURRENT_COMMIT="$(git rev-parse HEAD)" + + if git rev-parse "$TAG" >/dev/null 2>&1; then + TAG_COMMIT="$(git rev-list -n 1 "$TAG")" + + if [ "$TAG_COMMIT" != "$CURRENT_COMMIT" ]; then + echo "Tag $TAG already exists but points to $TAG_COMMIT, not current merged commit $CURRENT_COMMIT." + exit 1 + fi + + echo "Tag $TAG already exists and points to the current merged commit. Skipping tag creation." + exit 0 + fi + + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + + - name: Create GitHub release with generated title and notes + if: steps.target.outputs.should_promote == 'true' + uses: actions/github-script@v8 + env: + TAG_NAME: ${{ steps.version.outputs.tag }} + TARGET_COMMITISH: ${{ github.event.pull_request.merge_commit_sha }} + with: + script: | + const tagName = process.env.TAG_NAME; + const targetCommitish = process.env.TARGET_COMMITISH; + const { owner, repo } = context.repo; + + try { + const existingRelease = await github.rest.repos.getReleaseByTag({ + owner, + repo, + tag: tagName + }); + + core.info( + `Release already exists for ${tagName}: ${existingRelease.data.html_url}. Skipping release creation.` + ); + return; + } catch (error) { + if (error.status !== 404) { + throw error; + } + } + + const generatedNotes = await github.rest.repos.generateReleaseNotes({ + owner, + repo, + tag_name: tagName, + target_commitish: targetCommitish, + previous_tag_name: undefined + }); + + const release = await github.rest.repos.createRelease({ + owner, + repo, + tag_name: tagName, + target_commitish: targetCommitish, + name: generatedNotes.data.name, + body: generatedNotes.data.body, + draft: false, + prerelease: false + }); + + core.info(`Created release: ${release.data.html_url}`); + + - name: Promote merged commit to target branch + if: steps.target.outputs.should_promote == 'true' + shell: bash + run: | + git push origin HEAD:${{ steps.target.outputs.target_branch }} diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..9f5f03a --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,29 @@ +{ + "branches": ["version-15"], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "angular", + "releaseRules": [ + { "breaking": true, "release": false } + ] + } + ], + "@semantic-release/release-notes-generator", + [ + "@semantic-release/exec", + { + "prepareCmd": "sed -ir -E \"s/\\\"[0-9]+\\.[0-9]+\\.[0-9]+\\\"/\\\"${nextRelease.version}\\\"/\" servicems/__init__.py" + } + ], + [ + "@semantic-release/git", + { + "assets": ["servicems/__init__.py"], + "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" + } + ], + "@semantic-release/github" + ] +}