Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
232 changes: 232 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [main]
pull_request:
branches: [main]
# Default types miss title-only edits. Label changes must also re-run the
# version suggestion because the prerelease channel comes from a label.
types: [opened, edited, reopened, synchronize, labeled, unlabeled]
schedule:
# Weekly cargo-audit sweep for advisories disclosed after dependencies land.
# Use an off-peak minute rather than :00 or :30.
Expand Down Expand Up @@ -34,6 +37,15 @@ jobs:
- name: Check formatting
run: cargo fmt --all -- --check

validate-manifest:
name: Validate .tabularium manifest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Validate against the live registry schema
run: npx --yes @tabularium/cli validate .tabularium --registry https://registry.tabularis.dev --kind driver

live-db-integration:
name: Live SQL Server integration
runs-on: ubuntu-latest
Expand Down Expand Up @@ -73,6 +85,226 @@ jobs:
SQLSERVER_TEST_DATABASE: tabularis_test
run: cargo test --test live_db -- --test-threads=1

pr-title:
name: PR title (Conventional Commits)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

version-suggestion:
name: Version suggestion
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

# Classify the PR title's Conventional Commits type + breaking-change
# flag into a version-bump class. Requires the prerelease:* label to
# know which channel (alpha/beta/rc/stable) to suggest — see README's
# "Contributing: PR Titles & Versioning" for the full convention.
- name: Classify PR title and resolve prerelease channel
id: classify
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_LABELS: ${{ toJson(github.event.pull_request.labels) }}
run: |
PATTERN='^([a-zA-Z]+)(\(([^)]+)\))?(!)?: (.+)$'
if [[ "$PR_TITLE" =~ $PATTERN ]]; then
TYPE="${BASH_REMATCH[1]}"
BANG="${BASH_REMATCH[4]}"
else
echo "::error::PR title does not match Conventional Commits format (type: subject) — cannot classify."
exit 1
fi

BREAKING=false
[ -n "$BANG" ] && BREAKING=true
if echo "$PR_BODY" | grep -qiE "^BREAKING[ -]CHANGE:"; then
BREAKING=true
fi

case "$TYPE" in
feat) CLASS=minor ;;
fix|refactor|perf) CLASS=patch ;;
docs|style|chore|test|ci|build) CLASS=none ;;
*) CLASS=none ;;
esac
[ "$BREAKING" = true ] && CLASS=major

CHANNEL=$(echo "$PR_LABELS" | jq -r '[.[] | select(.name | startswith("prerelease:")) | .name][0] // ""' | sed 's/^prerelease://')
if [ -z "$CHANNEL" ]; then
echo "::error::No prerelease:alpha|beta|rc|stable label found on this PR. Add one so the version suggestion knows which channel to target — see README's 'Contributing: PR Titles & Versioning'."
exit 1
fi
case "$CHANNEL" in
alpha|beta|rc|stable) ;;
*) echo "::error::Unrecognized prerelease label value '$CHANNEL' — expected alpha, beta, rc, or stable."; exit 1 ;;
esac

echo "type=$TYPE" >> "$GITHUB_OUTPUT"
echo "breaking=$BREAKING" >> "$GITHUB_OUTPUT"
echo "class=$CLASS" >> "$GITHUB_OUTPUT"
echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT"

- name: Resolve baseline version
id: baseline
run: |
git fetch origin main --tags --quiet
TAG=$(git -C . describe --tags --abbrev=0 origin/main 2>/dev/null || true)
if [ -n "$TAG" ]; then
BASELINE="${TAG#v}"
else
BASELINE=$(git show origin/main:.tabularium | jq -r .version)
fi
echo "version=$BASELINE" >> "$GITHUB_OUTPUT"

- name: Compute suggestion, manage comment
uses: actions/github-script@v9
with:
script: |
const classification = "${{ steps.classify.outputs.class }}";
const channel = "${{ steps.classify.outputs.channel }}";
const type = "${{ steps.classify.outputs.type }}";
const breaking = "${{ steps.classify.outputs.breaking }}" === "true";
const baselineStr = "${{ steps.baseline.outputs.version }}";
const marker = "<!-- version-suggestion-bot";

function parseVersion(v) {
const m = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z]+)\.(\d+))?$/);
if (!m) throw new Error(`Cannot parse version: ${v}`);
return {
major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]),
stage: m[4] || null, stageNum: m[5] ? Number(m[5]) : null,
};
}
function formatVersion(v) {
const base = `${v.major}.${v.minor}.${v.patch}`;
return v.stage ? `${base}-${v.stage}.${v.stageNum}` : base;
}
function bumpStable(v, cls) {
const out = { major: v.major, minor: v.minor, patch: v.patch, stage: null, stageNum: null };
if (cls === "major") { out.major += 1; out.minor = 0; out.patch = 0; }
else if (cls === "minor") { out.minor += 1; out.patch = 0; }
else if (cls === "patch") { out.patch += 1; }
return out;
}
function computeNextVersion(baselineStr, classification, channelLabel) {
const baseline = parseVersion(baselineStr);
if (channelLabel === "stable") {
if (baseline.stage) {
return formatVersion({ major: baseline.major, minor: baseline.minor, patch: baseline.patch, stage: null, stageNum: null });
}
return formatVersion(bumpStable(baseline, classification));
}
if (baseline.stage === channelLabel) {
return formatVersion({ ...baseline, stageNum: baseline.stageNum + 1 });
}
let base = { major: baseline.major, minor: baseline.minor, patch: baseline.patch };
if (!baseline.stage) {
const bumped = bumpStable(baseline, classification);
base = { major: bumped.major, minor: bumped.minor, patch: bumped.patch };
}
return formatVersion({ ...base, stage: channelLabel, stageNum: 1 });
}

const prNumber = context.payload.pull_request.number;

// Find our most recent, not-yet-minimized comment on this PR.
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
const ours = comments.filter(c => c.body.includes(marker));
const previous = ours.length ? ours[ours.length - 1] : null;

let previousClassification = null;
if (previous) {
const m = previous.body.match(/classification=([\w-]+:[\w-]+:[\w-]+)/);
previousClassification = m ? m[1] : null;
}
const currentClassification = `${type}:${classification}:${channel}`;

if (classification === "none") {
if (previous && previousClassification !== currentClassification) {
// Was suggesting something (or saying "none" for a different
// reason/channel), now saying "none" for this reason — say so
// once, then stop.
await minimizePrevious();
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `No release needed for this PR (\`${type}\`).\n\n${marker} classification=${currentClassification} -->`,
});
}
// Otherwise: never suggested anything, or already said "none" — stay silent.
return;
}

if (previous && previousClassification === currentClassification) {
// Meaningful classification hasn't changed since the last comment.
return;
}

async function minimizePrevious() {
if (!previous) return;
// REST comment objects expose node_id directly — no separate
// lookup needed to get the GraphQL node id.
await github.graphql(
`mutation($id: ID!) { minimizeComment(input: { subjectId: $id, classifier: OUTDATED }) { clientMutationId } }`,
{ id: previous.node_id }
);
}

const suggested = computeNextVersion(baselineStr, classification, channel);
const tag = `v${suggested}`;

await minimizePrevious();

const breakingNote = breaking ? " (breaking change)" : "";
const body = [
`### Version suggestion`,
``,
`Based on this PR's title (\`${type}\`${breakingNote}) and the \`prerelease:${channel}\` label:`,
``,
`| | |`,
`|---|---|`,
`| Current | \`${baselineStr}\` |`,
`| Suggested next tag | \`${tag}\` |`,
``,
`This is informational only — no tag or release is created automatically yet.`,
``,
`${marker} classification=${currentClassification} -->`,
].join("\n");

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});

markdownlint:
name: Markdown lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Run markdownlint
run: npx --yes markdownlint-cli "**/*.md"

audit:
name: Security audit
runs-on: ubuntu-latest
Expand Down
60 changes: 57 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,25 @@ on:
- "v*"

jobs:
validate:
name: Validate release version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Check tag matches manifest version
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
TABULARIUM_VERSION=$(jq -r .version .tabularium)

if [ "$TAG_VERSION" != "$TABULARIUM_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match .tabularium version ($TABULARIUM_VERSION)"
exit 1
fi

build:
name: ${{ matrix.platform-label }}
needs: validate
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -41,7 +58,7 @@ jobs:
binary-suffix: ".exe"

steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand All @@ -58,8 +75,18 @@ jobs:
echo "present=false" >> "$GITHUB_OUTPUT"
fi

- name: Check for EXPLAIN parser
id: explain
shell: bash
run: |
if [ -f explain/package.json ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
fi

- name: Setup Node
if: steps.ui.outputs.present == 'true'
if: steps.ui.outputs.present == 'true' || steps.explain.outputs.present == 'true'
uses: actions/setup-node@v5
with:
node-version: "20"
Expand All @@ -72,6 +99,14 @@ jobs:
npm install --no-audit --no-fund
npm run build

- name: Build EXPLAIN parser
if: steps.explain.outputs.present == 'true'
shell: bash
working-directory: explain
run: |
npm install --no-audit --no-fund
npm run build

- name: Install cross (linux-arm64 only)
if: matrix.cross
run: cargo install cross --locked
Expand All @@ -98,6 +133,10 @@ jobs:
mkdir -p "$STAGE/ui/dist"
cp ui/dist/index.js "$STAGE/ui/dist/"
fi
if [ -f explain/dist/index.js ]; then
mkdir -p "$STAGE/explain/dist"
cp explain/dist/index.js "$STAGE/explain/dist/"
fi
(cd "$STAGE" && zip -r ../sqlserver-plugin-${{ matrix.platform-label }}.zip .)

- name: Package (windows)
Expand All @@ -115,6 +154,10 @@ jobs:
New-Item -ItemType Directory -Force -Path "$stage\ui\dist" | Out-Null
Copy-Item "ui\dist\index.js" "$stage\ui\dist"
}
if (Test-Path "explain\dist\index.js") {
New-Item -ItemType Directory -Force -Path "$stage\explain\dist" | Out-Null
Copy-Item "explain\dist\index.js" "$stage\explain\dist"
}
Compress-Archive -Path "$stage\*" -DestinationPath "sqlserver-plugin-${{ matrix.platform-label }}.zip"

- name: Stash artifact
Expand All @@ -132,14 +175,23 @@ jobs:
contents: write
steps:
- name: Checkout (for the .tabularium manifest asset)
uses: actions/checkout@v5
uses: actions/checkout@v7

- name: Download all build artifacts
uses: actions/download-artifact@v5
with:
path: artifacts
merge-multiple: true

- name: Detect prerelease from tag
id: meta
run: |
if [[ "${{ github.ref_name }}" == *-* ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi

- name: Publish release
uses: softprops/action-gh-release@v2
with:
Expand All @@ -148,3 +200,5 @@ jobs:
files: |
artifacts/*.zip
.tabularium
prerelease: ${{ steps.meta.outputs.prerelease == 'true' }}
make_latest: ${{ steps.meta.outputs.prerelease == 'false' }}
8 changes: 8 additions & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"default": true,
"MD013": false,
"MD024": { "siblings_only": true },
"MD033": false,
"MD041": false,
"MD060": false
}
2 changes: 2 additions & 0 deletions .markdownlintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
node_modules/
Loading