snapshot of https://github.com/casbin/casbin.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
248
.github/scripts/benchmark_formatter.py
vendored
Normal file
248
.github/scripts/benchmark_formatter.py
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
import pathlib, re, sys
|
||||
|
||||
try:
|
||||
p = pathlib.Path("comparison.md")
|
||||
if not p.exists():
|
||||
print("comparison.md not found, skipping post-processing.")
|
||||
sys.exit(0)
|
||||
|
||||
lines = p.read_text(encoding="utf-8").splitlines()
|
||||
processed_lines = []
|
||||
in_code = False
|
||||
def strip_worker_suffix(text: str) -> str:
|
||||
return re.sub(r'(\S+?)-\d+(\s|$)', r'\1\2', text)
|
||||
|
||||
def get_icon(diff_val: float) -> str:
|
||||
if diff_val > 10:
|
||||
return "🐌"
|
||||
if diff_val < -10:
|
||||
return "🚀"
|
||||
return "➡️"
|
||||
|
||||
def clean_superscripts(text: str) -> str:
|
||||
return re.sub(r'[¹²³⁴⁵⁶⁷⁸⁹⁰]', '', text)
|
||||
|
||||
def parse_val(token: str):
|
||||
if '%' in token or '=' in token:
|
||||
return None
|
||||
token = clean_superscripts(token)
|
||||
token = token.split('±')[0].strip()
|
||||
token = token.split('(')[0].strip()
|
||||
if not token:
|
||||
return None
|
||||
|
||||
m = re.match(r'^([-+]?\d*\.?\d+)([a-zA-Zµ]+)?$', token)
|
||||
if not m:
|
||||
return None
|
||||
try:
|
||||
val = float(m.group(1))
|
||||
except ValueError:
|
||||
return None
|
||||
suffix = (m.group(2) or "").replace("µ", "u")
|
||||
multipliers = {
|
||||
"n": 1e-9,
|
||||
"ns": 1e-9,
|
||||
"u": 1e-6,
|
||||
"us": 1e-6,
|
||||
"m": 1e-3,
|
||||
"ms": 1e-3,
|
||||
"s": 1.0,
|
||||
"k": 1e3,
|
||||
"K": 1e3,
|
||||
"M": 1e6,
|
||||
"G": 1e9,
|
||||
"Ki": 1024.0,
|
||||
"Mi": 1024.0**2,
|
||||
"Gi": 1024.0**3,
|
||||
"Ti": 1024.0**4,
|
||||
"B": 1.0,
|
||||
"B/op": 1.0,
|
||||
"C": 1.0, # tolerate degree/unit markers that don't affect ratio
|
||||
}
|
||||
return val * multipliers.get(suffix, 1.0)
|
||||
|
||||
def extract_two_numbers(tokens):
|
||||
found = []
|
||||
for t in tokens[1:]: # skip name
|
||||
if t in {"±", "∞", "~", "│", "│"}:
|
||||
continue
|
||||
if '%' in t or '=' in t:
|
||||
continue
|
||||
val = parse_val(t)
|
||||
if val is not None:
|
||||
found.append(val)
|
||||
if len(found) == 2:
|
||||
break
|
||||
return found
|
||||
|
||||
# Pass 0:
|
||||
# 1. find a header line with pipes to derive alignment hint
|
||||
# 2. calculate max content width to ensure right-most alignment
|
||||
max_content_width = 0
|
||||
|
||||
for line in lines:
|
||||
if line.strip() == "```":
|
||||
in_code = not in_code
|
||||
continue
|
||||
if not in_code:
|
||||
continue
|
||||
|
||||
# Skip footnotes/meta for width calculation
|
||||
if re.match(r'^\s*[¹²³⁴⁵⁶⁷⁸⁹⁰]', line) or re.search(r'need\s*>?=\s*\d+\s+samples', line):
|
||||
continue
|
||||
if not line.strip() or line.strip().startswith(('goos:', 'goarch:', 'pkg:', 'cpu:')):
|
||||
continue
|
||||
# Header lines are handled separately in Pass 1
|
||||
if '│' in line and ('vs base' in line or 'old' in line or 'new' in line):
|
||||
continue
|
||||
|
||||
# It's likely a data line
|
||||
# Check if it has an existing percentage we might move/align
|
||||
curr_line = strip_worker_suffix(line).rstrip()
|
||||
pct_match = re.search(r'([+-]?\d+\.\d+)%', curr_line)
|
||||
if pct_match:
|
||||
# If we are going to realign this, we count width up to the percentage
|
||||
w = len(curr_line[:pct_match.start()].rstrip())
|
||||
else:
|
||||
w = len(curr_line)
|
||||
|
||||
if w > max_content_width:
|
||||
max_content_width = w
|
||||
|
||||
# Calculate global alignment target for Diff column
|
||||
# Ensure target column is beyond the longest line with some padding
|
||||
diff_col_start = max_content_width + 4
|
||||
|
||||
# Calculate right boundary (pipe) position
|
||||
# Diff column width ~12 chars (e.g. "+100.00% 🚀")
|
||||
right_boundary = diff_col_start + 14
|
||||
|
||||
# Reset code fence tracking state for Pass 1
|
||||
in_code = False
|
||||
for line in lines:
|
||||
|
||||
if line.strip() == "```":
|
||||
in_code = not in_code
|
||||
processed_lines.append(line)
|
||||
continue
|
||||
|
||||
if not in_code:
|
||||
processed_lines.append(line)
|
||||
continue
|
||||
|
||||
# footnotes keep untouched
|
||||
if re.match(r'^\s*[¹²³⁴⁵⁶⁷⁸⁹⁰]', line) or re.search(r'need\s*>?=\s*\d+\s+samples', line):
|
||||
processed_lines.append(line)
|
||||
continue
|
||||
|
||||
# header lines: ensure last column labeled Diff and force alignment
|
||||
if '│' in line and ('vs base' in line or 'old' in line or 'new' in line):
|
||||
# Strip trailing pipe and whitespace
|
||||
stripped_header = line.rstrip().rstrip('│').rstrip()
|
||||
|
||||
# If "vs base" is present, ensure we don't duplicate "Diff" if it's already there
|
||||
# But we want to enforce OUR alignment, so we might strip existing Diff
|
||||
stripped_header = re.sub(r'\s+Diff\s*$', '', stripped_header, flags=re.IGNORECASE)
|
||||
stripped_header = re.sub(r'\s+Delta\b', '', stripped_header, flags=re.IGNORECASE)
|
||||
|
||||
# Pad to diff_col_start
|
||||
if len(stripped_header) < diff_col_start:
|
||||
new_header = stripped_header + " " * (diff_col_start - len(stripped_header))
|
||||
else:
|
||||
new_header = stripped_header + " "
|
||||
|
||||
# Add Diff column header if it's the second header row (vs base)
|
||||
if 'vs base' in line:
|
||||
new_header += "Diff"
|
||||
|
||||
# Add closing pipe at the right boundary
|
||||
current_len = len(new_header)
|
||||
if current_len < right_boundary:
|
||||
new_header += " " * (right_boundary - current_len)
|
||||
|
||||
new_header += "│"
|
||||
processed_lines.append(new_header)
|
||||
continue
|
||||
|
||||
# non-data meta lines
|
||||
if not line.strip() or line.strip().startswith(('goos:', 'goarch:', 'pkg:')):
|
||||
processed_lines.append(line)
|
||||
continue
|
||||
|
||||
line = strip_worker_suffix(line)
|
||||
tokens = line.split()
|
||||
if not tokens:
|
||||
processed_lines.append(line)
|
||||
continue
|
||||
|
||||
numbers = extract_two_numbers(tokens)
|
||||
pct_match = re.search(r'([+-]?\d+\.\d+)%', line)
|
||||
|
||||
# Helper to align and append
|
||||
def append_aligned(left_part, content):
|
||||
if len(left_part) < diff_col_start:
|
||||
aligned = left_part + " " * (diff_col_start - len(left_part))
|
||||
else:
|
||||
aligned = left_part + " "
|
||||
|
||||
# Ensure content doesn't exceed right boundary (visual check only, we don't truncate)
|
||||
# But users asked not to exceed header pipe.
|
||||
# Header pipe is at right_boundary.
|
||||
# Content starts at diff_col_start.
|
||||
# So content length should be <= right_boundary - diff_col_start
|
||||
return f"{aligned}{content}"
|
||||
|
||||
# Special handling for geomean when values missing or zero
|
||||
is_geomean = tokens[0] == "geomean"
|
||||
if is_geomean and (len(numbers) < 2 or any(v == 0 for v in numbers)) and not pct_match:
|
||||
leading = re.match(r'^\s*', line).group(0)
|
||||
left = f"{leading}geomean"
|
||||
processed_lines.append(append_aligned(left, "n/a (has zero)"))
|
||||
continue
|
||||
|
||||
# when both values are zero, force diff = 0 and align
|
||||
if len(numbers) == 2 and numbers[0] == 0 and numbers[1] == 0:
|
||||
diff_val = 0.0
|
||||
icon = get_icon(diff_val)
|
||||
left = line.rstrip()
|
||||
processed_lines.append(append_aligned(left, f"{diff_val:+.2f}% {icon}"))
|
||||
continue
|
||||
|
||||
# recompute diff when we have two numeric values
|
||||
if len(numbers) == 2 and numbers[0] != 0:
|
||||
diff_val = (numbers[1] - numbers[0]) / numbers[0] * 100
|
||||
icon = get_icon(diff_val)
|
||||
|
||||
left = line
|
||||
if pct_match:
|
||||
left = line[:pct_match.start()].rstrip()
|
||||
else:
|
||||
left = line.rstrip()
|
||||
|
||||
processed_lines.append(append_aligned(left, f"{diff_val:+.2f}% {icon}"))
|
||||
continue
|
||||
|
||||
# fallback: align existing percentage to Diff column and (re)append icon
|
||||
if pct_match:
|
||||
try:
|
||||
pct_val = float(pct_match.group(1))
|
||||
icon = get_icon(pct_val)
|
||||
|
||||
left = line[:pct_match.start()].rstrip()
|
||||
suffix = line[pct_match.end():]
|
||||
# Remove any existing icon after the percentage to avoid duplicates
|
||||
suffix = re.sub(r'\s*(🐌|🚀|➡️)', '', suffix)
|
||||
|
||||
processed_lines.append(append_aligned(left, f"{pct_val:+.2f}% {icon}{suffix}"))
|
||||
except ValueError:
|
||||
processed_lines.append(line)
|
||||
continue
|
||||
|
||||
# If we cannot parse numbers or percentages, keep the original (only worker suffix stripped)
|
||||
processed_lines.append(line)
|
||||
|
||||
p.write_text("\n".join(processed_lines) + "\n", encoding="utf-8")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error post-processing comparison.md: {e}")
|
||||
sys.exit(1)
|
||||
32
.github/scripts/download_artifact.js
vendored
Normal file
32
.github/scripts/download_artifact.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
module.exports = async ({github, context, core}) => {
|
||||
try {
|
||||
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
|
||||
const matchArtifact = artifacts.data.artifacts.find((artifact) => {
|
||||
return artifact.name == "benchmark-results";
|
||||
});
|
||||
|
||||
if (!matchArtifact) {
|
||||
core.setFailed("No artifact named 'benchmark-results' found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const workspace = process.env.GITHUB_WORKSPACE;
|
||||
fs.writeFileSync(path.join(workspace, 'benchmark-results.zip'), Buffer.from(download.data));
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to download artifact: ${error.message}`);
|
||||
}
|
||||
};
|
||||
59
.github/scripts/post_comment.js
vendored
Normal file
59
.github/scripts/post_comment.js
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
module.exports = async ({github, context, core}) => {
|
||||
const fs = require('fs');
|
||||
|
||||
// Validate pr_number.txt
|
||||
if (!fs.existsSync('pr_number.txt')) {
|
||||
core.setFailed("Required artifact file 'pr_number.txt' was not found in the workspace.");
|
||||
return;
|
||||
}
|
||||
const prNumberContent = fs.readFileSync('pr_number.txt', 'utf8').trim();
|
||||
const issue_number = parseInt(prNumberContent, 10);
|
||||
if (!Number.isFinite(issue_number) || issue_number <= 0) {
|
||||
core.setFailed('Invalid PR number in pr_number.txt: "' + prNumberContent + '"');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate comparison.md
|
||||
if (!fs.existsSync('comparison.md')) {
|
||||
core.setFailed("Required artifact file 'comparison.md' was not found in the workspace.");
|
||||
return;
|
||||
}
|
||||
let comparison;
|
||||
try {
|
||||
comparison = fs.readFileSync('comparison.md', 'utf8');
|
||||
} catch (error) {
|
||||
core.setFailed("Failed to read 'comparison.md': " + error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find existing comment
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
});
|
||||
|
||||
const botComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('Benchmark Comparison')
|
||||
);
|
||||
|
||||
const footer = '<sub>🤖 This comment will be automatically updated with the latest benchmark results.</sub>';
|
||||
const commentBody = `${comparison}\n\n${footer}`;
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: commentBody
|
||||
});
|
||||
}
|
||||
};
|
||||
2
.github/semantic.yml
vendored
Normal file
2
.github/semantic.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Always validate the PR title AND all the commits
|
||||
titleAndCommits: true
|
||||
37
.github/workflows/comment.yml
vendored
Normal file
37
.github/workflows/comment.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Post Benchmark Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Performance Comparison for Pull Requests"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 'Download artifact'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/download_artifact.js')
|
||||
await script({github, context, core})
|
||||
|
||||
- name: 'Unzip artifact'
|
||||
run: unzip benchmark-results.zip
|
||||
|
||||
- name: 'Post comment'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/post_comment.js')
|
||||
await script({github, context, core})
|
||||
44
.github/workflows/default.yml
vendored
Normal file
44
.github/workflows/default.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: ['1.21']
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Run go test
|
||||
run: make test
|
||||
|
||||
benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: ['1.21']
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Run go test bench
|
||||
run: make benchmark
|
||||
26
.github/workflows/golangci-lint.yml
vendored
Normal file
26
.github/workflows/golangci-lint.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: golangci-lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
97
.github/workflows/performance-pr.yml
vendored
Normal file
97
.github/workflows/performance-pr.yml
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
name: Performance Comparison for Pull Requests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
benchmark-pr:
|
||||
name: Performance benchmark comparison
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
|
||||
# Save commit SHAs for display
|
||||
- name: Save commit info
|
||||
id: commits
|
||||
run: |
|
||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
||||
echo "base_short=${BASE_SHA:0:7}" >> $GITHUB_OUTPUT
|
||||
echo "head_short=${HEAD_SHA:0:7}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Run benchmark on PR branch
|
||||
- name: Run benchmark on PR branch
|
||||
run: |
|
||||
go test -bench '.' -benchtime=2s -benchmem ./... | tee pr-bench.txt
|
||||
|
||||
# Checkout base branch and run benchmark
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
clean: false
|
||||
path: base
|
||||
|
||||
- name: Run benchmark on base branch
|
||||
working-directory: base
|
||||
run: |
|
||||
go test -bench '.' -benchtime=2s -benchmem ./... | \
|
||||
tee ../base-bench.txt
|
||||
|
||||
# Install benchstat for comparison
|
||||
- name: Install benchstat
|
||||
run: go install golang.org/x/perf/cmd/benchstat@latest
|
||||
|
||||
# Compare benchmarks using benchstat
|
||||
- name: Compare benchmarks with benchstat
|
||||
id: benchstat
|
||||
run: |
|
||||
cat > comparison.md << 'EOF'
|
||||
## Benchmark Comparison
|
||||
|
||||
Comparing base branch (`${{ steps.commits.outputs.base_short }}`)
|
||||
vs PR branch (`${{ steps.commits.outputs.head_short }}`)
|
||||
|
||||
```
|
||||
EOF
|
||||
benchstat base-bench.txt pr-bench.txt >> comparison.md || true
|
||||
echo '```' >> comparison.md
|
||||
|
||||
# Post-process to append percentage + emoji column (🚀 faster < -10%, 🐌 slower > +10%, otherwise ➡️)
|
||||
if [ ! -f comparison.md ]; then
|
||||
echo "comparison.md not found after benchstat." >&2
|
||||
exit 1
|
||||
fi
|
||||
python3 .github/scripts/benchmark_formatter.py
|
||||
|
||||
# Save PR number
|
||||
- name: Save PR number
|
||||
run: |
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "Error: Pull request number is not available in event payload." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$PR_NUMBER" > pr_number.txt
|
||||
|
||||
# Upload benchmark results
|
||||
- name: Upload benchmark results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark-results
|
||||
path: |
|
||||
comparison.md
|
||||
pr_number.txt
|
||||
204
.github/workflows/release.yml
vendored
Normal file
204
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# Releases are driven by manually pushed tags only. Nothing is published
|
||||
# automatically when a pull request is merged.
|
||||
#
|
||||
# v<X>.<Y>.<Z>-rc<N> GitHub pre-release only.
|
||||
# v<X>.<Y>.<Z> GitHub release plus the Go module proxy.
|
||||
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository == 'apache/casbin' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Compute release metadata
|
||||
id: release
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
TAG_NAME="${GITHUB_REF_NAME}"
|
||||
if [[ "${TAG_NAME}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
IS_RC="false"
|
||||
elif [[ "${TAG_NAME}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)-rc[0-9]+$ ]]; then
|
||||
IS_RC="true"
|
||||
else
|
||||
echo "::error::Unsupported release tag: ${TAG_NAME}. Expected vX.Y.Z or vX.Y.Z-rcN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="${BASH_REMATCH[1]}"
|
||||
BASENAME="apache-casbin-incubating-${VERSION}-src"
|
||||
|
||||
# Previous release, used as the start of the change log range.
|
||||
PREVIOUS_TAG="$(
|
||||
{
|
||||
git tag --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true
|
||||
echo "v${VERSION}"
|
||||
} |
|
||||
sort -V -u |
|
||||
awk -v current="v${VERSION}" '
|
||||
$0 == current {
|
||||
print previous
|
||||
exit
|
||||
}
|
||||
{
|
||||
previous = $0
|
||||
}
|
||||
'
|
||||
)"
|
||||
|
||||
{
|
||||
echo "tag=${TAG_NAME}"
|
||||
echo "version=${VERSION}"
|
||||
echo "is_rc=${IS_RC}"
|
||||
echo "basename=${BASENAME}"
|
||||
echo "previous_tag=${PREVIOUS_TAG}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
CURRENT_TAG="${{ steps.release.outputs.tag }}"
|
||||
CURRENT_VERSION="${CURRENT_TAG#v}"
|
||||
PREVIOUS_TAG="${{ steps.release.outputs.previous_tag }}"
|
||||
RELEASE_DATE="$(date -u +%F)"
|
||||
RANGE="${CURRENT_TAG}"
|
||||
|
||||
if [ -n "${PREVIOUS_TAG}" ]; then
|
||||
RANGE="${PREVIOUS_TAG}..${CURRENT_TAG}"
|
||||
fi
|
||||
|
||||
FEATURES="$(git log --pretty=format:'%s (%h)' "${RANGE}" | grep -Ei '^(feat)(\(.+\))?: ' || true)"
|
||||
FIXES="$(git log --pretty=format:'%s (%h)' "${RANGE}" | grep -Ei '^(fix)(\(.+\))?: ' || true)"
|
||||
DOCS="$(git log --pretty=format:'%s (%h)' "${RANGE}" | grep -Ei '^(docs?|doc)(\(.+\))?: ' || true)"
|
||||
|
||||
{
|
||||
if [ -n "${PREVIOUS_TAG}" ]; then
|
||||
echo "# [${CURRENT_VERSION}](https://github.com/${GITHUB_REPOSITORY}/compare/${PREVIOUS_TAG}...${CURRENT_TAG}) (${RELEASE_DATE})"
|
||||
else
|
||||
echo "# ${CURRENT_VERSION} (${RELEASE_DATE})"
|
||||
fi
|
||||
echo
|
||||
if [ "${{ steps.release.outputs.is_rc }}" = "true" ]; then
|
||||
echo "Release candidate for ${{ steps.release.outputs.version }}. This is not an official release."
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ -n "${FEATURES}" ]; then
|
||||
echo "## Features"
|
||||
echo
|
||||
printf '%s\n' "${FEATURES}" | sed 's/^/- /'
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ -n "${FIXES}" ]; then
|
||||
echo "## Fixes"
|
||||
echo
|
||||
printf '%s\n' "${FIXES}" | sed 's/^/- /'
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ -n "${DOCS}" ]; then
|
||||
echo "## Docs"
|
||||
echo
|
||||
printf '%s\n' "${DOCS}" | sed 's/^/- /'
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ -z "${FEATURES}${FIXES}${DOCS}" ]; then
|
||||
echo "## Notes"
|
||||
echo
|
||||
echo "- No feat/fix/doc commits were found in this release range."
|
||||
fi
|
||||
} > RELEASE_NOTES.md
|
||||
|
||||
- name: Create source archives
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BASENAME="${{ steps.release.outputs.basename }}"
|
||||
mkdir -p dist
|
||||
git archive --format=tar.gz --prefix="${BASENAME}/" -o "dist/${BASENAME}.tar.gz" "${{ steps.release.outputs.tag }}"
|
||||
git archive --format=zip --prefix="${BASENAME}/" -o "dist/${BASENAME}.zip" "${{ steps.release.outputs.tag }}"
|
||||
|
||||
cd dist
|
||||
for archive in "${BASENAME}.tar.gz" "${BASENAME}.zip"; do
|
||||
sha512sum "${archive}" > "${archive}.sha512"
|
||||
done
|
||||
sha512sum --check ./*.sha512
|
||||
|
||||
- name: Publish GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${{ steps.release.outputs.is_rc }}" = "true" ]; then
|
||||
TITLE="Apache Casbin (Incubating) ${{ steps.release.outputs.version }} (release candidate ${{ steps.release.outputs.tag }})"
|
||||
FLAGS=(--prerelease)
|
||||
else
|
||||
TITLE="Apache Casbin (Incubating) ${{ steps.release.outputs.version }}"
|
||||
FLAGS=(--latest)
|
||||
fi
|
||||
|
||||
gh release create "${{ steps.release.outputs.tag }}" \
|
||||
--verify-tag \
|
||||
--title "${TITLE}" \
|
||||
--notes-file RELEASE_NOTES.md \
|
||||
"${FLAGS[@]}" \
|
||||
dist/*
|
||||
|
||||
- name: Publish to the Go module proxy
|
||||
if: ${{ steps.release.outputs.is_rc == 'false' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
MODULE_PATH="$(awk '/^module /{print $2; exit}' go.mod)"
|
||||
TAG="${{ steps.release.outputs.tag }}"
|
||||
|
||||
# Requesting the version makes the proxy fetch and cache it, which is
|
||||
# what publishes the release to the Go module ecosystem.
|
||||
for attempt in 1 2 3; do
|
||||
if curl --fail --silent --show-error --location \
|
||||
"https://proxy.golang.org/${MODULE_PATH}/@v/${TAG}.info"; then
|
||||
echo
|
||||
echo "Published ${MODULE_PATH}@${TAG} to proxy.golang.org."
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt ${attempt} failed, retrying in 30s..."
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "::error::Failed to publish ${MODULE_PATH}@${TAG} to proxy.golang.org."
|
||||
exit 1
|
||||
Reference in New Issue
Block a user