feat(deploy): add email (SMTP) deployment provider #86
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "GitHub Pull Requests Checker" | |
| on: | |
| issue_comment: | |
| types: [created] | |
| concurrency: | |
| group: gh_pr_check-${{ github.event.issue.number }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| statuses: write | |
| jobs: | |
| pr-guard: | |
| if: > | |
| github.event.issue.pull_request && | |
| contains(fromJSON('["OWNER", "MEMBER"]'), github.event.comment.author_association) && | |
| startsWith(github.event.comment.body, '/pr-check') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Log | |
| run: | | |
| echo "Triggered by @${{ github.event.comment.user.login }}" | |
| echo "On PR #${{ github.event.issue.number }}" | |
| - name: Get PR details | |
| id: pr_details | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| }); | |
| core.setOutput("git_head_repo", pr.head.repo.full_name); | |
| core.setOutput("git_head_ref", pr.head.ref); | |
| core.setOutput("git_head_sha", pr.head.sha); | |
| console.log(`git head repo: ${pr.head.repo.full_name}, ref: ${pr.head.ref}, sha: ${pr.head.sha}`); | |
| outputs: | |
| git_head_repo: ${{ steps.pr_details.outputs.git_head_repo }} | |
| git_head_ref: ${{ steps.pr_details.outputs.git_head_ref }} | |
| git_head_sha: ${{ steps.pr_details.outputs.git_head_sha }} | |
| no_compile: ${{ contains(github.event.comment.body, '-no-compile') }} | |
| no_test: ${{ contains(github.event.comment.body, '-no-test') }} | |
| no_lint: ${{ contains(github.event.comment.body, '-no-lint') }} | |
| compile-build: | |
| if: needs.pr-guard.outputs.no_compile != 'true' | |
| needs: pr-guard | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Initialize Actions status | |
| if: matrix.os == 'ubuntu-latest' | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| await github.rest.repos.createCommitStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: "${{ needs.pr-guard.outputs.git_head_sha }}", | |
| state: "pending", | |
| context: "pr-check/compile-build", | |
| description: "Running...", | |
| target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| }); | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: refs/pull/${{ github.event.issue.number }}/merge | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| submodules: true | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: "go.mod" | |
| - name: Check Node.js dependencies | |
| run: | | |
| npm --prefix=./ui ci | |
| - name: Build Node.js | |
| run: | | |
| npm --prefix=./ui run build | |
| - name: Build Go | |
| env: | |
| CGO_ENABLED: 0 | |
| run: | | |
| go build -v -ldflags "-s -w" -trimpath -o ./dist/certimate ./ | |
| unit-test: | |
| if: needs.pr-guard.outputs.no_test != 'true' | |
| needs: pr-guard | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Initialize Actions status | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| await github.rest.repos.createCommitStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: "${{ needs.pr-guard.outputs.git_head_sha }}", | |
| state: "pending", | |
| context: "pr-check/unit-test", | |
| description: "Running...", | |
| target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| }); | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: refs/pull/${{ github.event.issue.number }}/merge | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| submodules: true | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: "go.mod" | |
| - name: Run Go unit tests | |
| id: gotest | |
| if: success() || failure() | |
| continue-on-error: true | |
| run: | | |
| # Exclude paths below: | |
| EXCLUDES=( | |
| "./pkg/core/certmgr/providers-tester/" | |
| "./pkg/core/deployer/providers-tester/" | |
| "./pkg/core/notifier/providers-tester/" | |
| "./pkg/core/purger/providers-tester/" | |
| "./pkg/sdk3rd-forked/" | |
| ) | |
| GO_PKGS=$(go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' ./... | grep -Ev $(IFS='|'; echo "${EXCLUDES[*]}")) | |
| echo " " | |
| echo "Testing packages:" | |
| echo "$GO_PKGS" | |
| echo " " | |
| if ! go test -race -v $GO_PKGS; then | |
| echo "::error::Failed to run 'go test', please fix the issues." >&2 | |
| exit 1 | |
| else | |
| go test -tags=tester -c -o /dev/null ./... | |
| echo "'go test' passed." | |
| fi | |
| - name: Check state | |
| if: always() | |
| run: | | |
| if [[ "${{ steps.gotest.outcome }}" == "failure" ]]; then | |
| echo "::error::Some cases failed." | |
| exit 1 | |
| fi | |
| lint-format: | |
| if: needs.pr-guard.outputs.no_lint != 'true' | |
| needs: pr-guard | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Initialize Actions status | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| await github.rest.repos.createCommitStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: "${{ needs.pr-guard.outputs.git_head_sha }}", | |
| state: "pending", | |
| context: "pr-check/lint-format", | |
| description: "Running...", | |
| target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| }); | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: refs/pull/${{ github.event.issue.number }}/merge | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| submodules: true | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: "go.mod" | |
| # 暂时禁用,避免 Prettier 配置冲突导致检查失败 | |
| # - name: Run Node.js code linter | |
| # id: jslint | |
| # if: success() || failure() | |
| # continue-on-error: true | |
| # run: | | |
| # git reset --hard HEAD | |
| # git clean -df | |
| # cd ui | |
| # npm ci | |
| # echo "Running 'eslint' ..." | |
| # if ! npx eslint . --quiet; then | |
| # echo "::error::'eslint' resulted in changes, please format files and commit them:" | |
| # exit 1 | |
| # else | |
| # echo "'eslint' passed." | |
| # fi | |
| - name: Run Node.js code formatter | |
| id: jsfmt | |
| if: success() || failure() | |
| continue-on-error: true | |
| run: | | |
| git reset --hard HEAD | |
| git clean -df | |
| cd ui | |
| npm ci | |
| echo "Running 'eslint --fix' ..." | |
| npx eslint . --fix --output-file=/dev/null | |
| if ! git diff --quiet; then | |
| echo "::error::'eslint --fix' resulted in changes, please format files and commit them:" | |
| git diff --exit-code | |
| exit 1 | |
| else | |
| echo "'eslint --fix' passed." | |
| fi | |
| - name: Run Go code linter | |
| id: golint | |
| if: success() || failure() | |
| continue-on-error: true | |
| run: | | |
| git reset --hard HEAD | |
| git clean -df | |
| # Exclude paths below: | |
| EXCLUDES=( | |
| "./pkg/sdk3rd-forked/" | |
| "./pkg/sdk3rd-trimmed/" | |
| ) | |
| GO_MODULE=$(go list -m) | |
| GO_PKGS=$(go list ./... | grep -Ev $(IFS='|'; echo "${EXCLUDES[*]}")) | |
| EXCLUDE_FUNC=() | |
| for pkg in ${GO_PKGS}; do | |
| pkg_path="./${pkg#$GO_MODULE/}" | |
| if [ ! -d "$pkg_path" ]; then | |
| EXCLUDE_FUNC+=("$pkg") | |
| continue | |
| fi | |
| if ! grep -RqE '//nolint:(all|govet)' "$pkg_path"/*.go 2>/dev/null; then | |
| EXCLUDE_FUNC+=("$pkg") | |
| fi | |
| done | |
| GO_PKGS=$(printf '%s\n' "${EXCLUDE_FUNC[@]}") | |
| echo " " | |
| echo "Linting packages:" | |
| echo "$GO_PKGS" | |
| echo " " | |
| echo "Running 'go vet' ..." | |
| if ! echo "$GO_PKGS" | xargs go vet; then | |
| echo "::error::Failed to run 'go vet', please fix the issues." | |
| exit 1 | |
| else | |
| echo "'go vet' passed." | |
| fi | |
| - name: Run Go code formatter | |
| id: gofmt | |
| if: success() || failure() | |
| continue-on-error: true | |
| run: | | |
| git reset --hard HEAD | |
| git clean -df | |
| go install mvdan.cc/gofumpt@latest | |
| gofumpt --version | |
| # Exclude paths below: | |
| EXCLUDES=( | |
| "./pkg/sdk3rd-forked/" | |
| "./pkg/sdk3rd-trimmed/" | |
| ) | |
| EXCLUDE_FUNC=() | |
| for ex in "${EXCLUDES[@]}"; do | |
| EXCLUDE_FUNC+=(-path "${ex}*" -prune -o) | |
| done | |
| GO_FILES="$( | |
| find . \ | |
| "${EXCLUDE_FUNC[@]}" \ | |
| -type f -name "*.go" \ | |
| -not -name "*.pb.go" \ | |
| )" | |
| echo " " | |
| echo "Formatting files:" | |
| echo "$GO_FILES" | |
| echo " " | |
| echo "Running 'gofumpt' ..." | |
| echo "$GO_FILES" | xargs -d '\n' gofumpt -l -w | |
| if ! git diff --quiet; then | |
| echo "::error::'gofumpt' resulted in changes, please format files and commit them:" | |
| git diff --exit-code | |
| exit 1 | |
| else | |
| echo "'gofumpt' passed." | |
| fi | |
| - name: Check state | |
| if: always() | |
| run: | | |
| if [[ "${{ steps.jslint.outcome }}" == "failure" || | |
| "${{ steps.jsfmt.outcome }}" == "failure" || | |
| "${{ steps.golint.outcome }}" == "failure" || | |
| "${{ steps.gofmt.outcome }}" == "failure" ]]; then | |
| echo "::error::Some cases failed." | |
| exit 1 | |
| fi | |
| ending: | |
| needs: [pr-guard, compile-build, unit-test, lint-format] | |
| if: always() && needs.pr-guard.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| statuses: write | |
| steps: | |
| - name: Compute overall state | |
| id: overall_state | |
| run: | | |
| # 最终状态: | |
| # - 任一失败 → failure | |
| # - 任一手动取消/超时 → cancelled | |
| # - 全部成功 → success | |
| RESULTS="${{ needs.compile-build.result }} ${{ needs.unit-test.result }} ${{ needs.lint-format.result }}" | |
| if echo "$RESULTS" | grep -q "failure\|error"; then | |
| echo "state=failure" >> "$GITHUB_OUTPUT" | |
| elif echo "$RESULTS" | grep -q "cancelled"; then | |
| echo "state=cancelled" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "state=success" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Set Actions status | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const jobs = [ | |
| { | |
| ctx: "pr-check/compile-build", | |
| result: "${{ needs.compile-build.result }}", | |
| skipped: "${{ needs.pr-guard.outputs.no_compile }}", | |
| }, | |
| { | |
| ctx: "pr-check/unit-test", | |
| result: "${{ needs.unit-test.result }}", | |
| skipped: "${{ needs.pr-guard.outputs.no_test }}", | |
| }, | |
| { | |
| ctx: "pr-check/lint-format", | |
| result: "${{ needs.lint-format.result }}", | |
| skipped: "${{ needs.pr-guard.outputs.no_lint }}", | |
| }, | |
| ]; | |
| const states = { | |
| "success": "success", | |
| "failure": "failure", | |
| "cancelled": "error" | |
| }; | |
| for (const j of jobs) { | |
| if (j.skipped === "true") continue; | |
| const state = states[j.result] || "error"; | |
| const desc = state === "success" ? "Passed" | |
| : state === "failure" ? "Failed" | |
| : "Cancelled"; | |
| await github.rest.repos.createCommitStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: "${{ needs.pr-guard.outputs.git_head_sha }}", | |
| state: state, | |
| context: j.ctx, | |
| description: desc, | |
| target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| }); | |
| } | |
| - name: Create comment | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const stateIcon = "${{ steps.overall_state.outputs.state }}" === "success" ? "✅" | |
| : "${{ steps.overall_state.outputs.state }}" === "failure" ? "❌" | |
| : ""; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: ${{ github.event.issue.number }}, | |
| body: stateIcon + "PR check done, overall result: **" + "${{ steps.overall_state.outputs.state }}" + "**\n" + | |
| "- _compile-build_: **" + ("${{ needs.pr-guard.outputs.no_compile }}" === "true" ? "skipped" : "${{ needs.compile-build.result }}") + "**\n" + | |
| "- _unit-test_: **" + ("${{ needs.pr-guard.outputs.no_test }}" === "true" ? "skipped" : "${{ needs.unit-test.result }}") + "**\n" + | |
| "- _lint-format_: **" + ("${{ needs.pr-guard.outputs.no_lint }}" === "true" ? "skipped" : "${{ needs.lint-format.result }}") + "**\n\n" + | |
| "Click to [view details](" + `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + ").", | |
| }); | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |