From c80edbf06bcd05cb2fe84909437a77775fb29358 Mon Sep 17 00:00:00 2001 From: mday-io Date: Sun, 1 Nov 2026 21:46:38 +0000 Subject: [PATCH 1/6] ci: group release notes by label and check PR titles Release notes were a flat list of PR titles, mixing CI and test work with user-facing changes and giving no callout for breaking changes or state migrations. - .github/release.yml groups generated notes into Breaking Changes, State Migrations, Features, Performance, Bug Fixes, Documentation and Maintenance, and leaves out ci, test, dependencies and skip-changelog PRs. - A new PR Labels workflow checks that PR titles follow 'type(scope): description' and labels the PR from the type, scope and '!'. actions/labeler adds migration, engine and area labels from changed files. - The PR template gains a Release Note section, CONTRIBUTING.md documents the title format, Dependabot uses a lowercase 'chore' prefix, and GitHub releases are created as drafts so highlights can be added before publishing. Signed-off-by: mday-io --- .github/dependabot.yml | 6 ++ .github/labeler.yml | 85 +++++++++++++++++++++++ .github/pull_request_template.md | 9 +++ .github/release.yml | 36 ++++++++++ .github/workflows/pr-labels.yaml | 113 +++++++++++++++++++++++++++++++ .github/workflows/release.yaml | 3 + CONTRIBUTING.md | 19 ++++++ 7 files changed, 271 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/pr-labels.yaml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5acbdac5d3..acd2e962c1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,13 @@ updates: directory: '/' schedule: interval: 'weekly' + commit-message: + prefix: 'chore' + include: 'scope' - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'weekly' + commit-message: + prefix: 'chore' + include: 'scope' diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..7221450f1b --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,85 @@ +# Path-based PR labels, applied by workflows/pr-labels.yaml. + +migration: + - changed-files: + - added-files: 'sqlmesh/migrations/v*.py' + +dbt: + - changed-files: + - any-glob-to-any-file: ['sqlmesh/dbt/**', 'sqlmesh_dbt/**'] + +vscode: + - changed-files: + - any-glob-to-any-file: ['vscode/**', 'sqlmesh/lsp/**'] + +web-ui: + - changed-files: + - any-glob-to-any-file: ['web/**'] + +cicd-bot: + - changed-files: + - any-glob-to-any-file: ['sqlmesh/integrations/github/**', 'sqlmesh/cicd/**'] + +engine:athena: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/athena.py' + +engine:bigquery: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/bigquery.py' + +engine:clickhouse: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/clickhouse.py' + +engine:databricks: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/databricks.py' + +engine:db2: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/db2.py' + +engine:duckdb: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/duckdb.py' + +engine:fabric: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/fabric.py' + +engine:mssql: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/mssql.py' + +engine:mysql: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/mysql.py' + +engine:postgres: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/postgres.py' + +engine:redshift: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/redshift.py' + +engine:risingwave: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/risingwave.py' + +engine:snowflake: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/snowflake.py' + +engine:spark: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/spark.py' + +engine:starrocks: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/starrocks.py' + +engine:trino: + - changed-files: + - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/trino.py' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7585f0ce10..7517edf6ef 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,12 +2,21 @@ +## Release Note + + + ## Test Plan ## Checklist +- [ ] My PR title follows the `type(scope): description` format (see [CONTRIBUTING.md](CONTRIBUTING.md#pull-request-titles)) - [ ] I have run `make style` and fixed any issues - [ ] I have added tests for my changes (if applicable) - [ ] All existing tests pass (`make fast-test`) diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000..c389bb2b4d --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,36 @@ +# Groups the notes that GitHub generates for each release (see +# `generate_release_notes` in workflows/release.yaml) by PR label. +# Labels are applied automatically by workflows/pr-labels.yaml. +# A PR is listed under the first category whose labels it matches. +changelog: + exclude: + labels: + - skip-changelog + - ci + - test + - dependencies + categories: + - title: ⚠️ Breaking Changes + labels: + - breaking-change + - title: 🗄️ State Migrations (run `sqlmesh migrate` after upgrading) + labels: + - migration + - title: 🚀 Features + labels: + - feature + - title: ⚡ Performance + labels: + - performance + - title: 🐛 Bug Fixes + labels: + - bug + - title: 📚 Documentation + labels: + - documentation + - title: 🧰 Maintenance + labels: + - maintenance + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml new file mode 100644 index 0000000000..87939e6227 --- /dev/null +++ b/.github/workflows/pr-labels.yaml @@ -0,0 +1,113 @@ +name: PR Labels +# Checks the PR title format and applies the labels that .github/release.yml +# uses to group release notes. +# +# pull_request_target is used so that labels can be applied to PRs from forks. +# This workflow never checks out or runs code from the PR. +on: + pull_request_target: + types: [opened, edited, reopened, synchronize] + +permissions: + contents: read + issues: write + pull-requests: write + +concurrency: + group: pr-labels-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + title: + name: Check title and label by type + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v8 + env: + PR_TITLE: ${{ github.event.pull_request.title }} + with: + script: | + const title = process.env.PR_TITLE; + const pr = context.payload.pull_request.number; + const { owner, repo } = context.repo; + + const typeLabels = { + feat: 'feature', + fix: 'bug', + perf: 'performance', + docs: 'documentation', + refactor: 'maintenance', + chore: 'maintenance', + build: 'maintenance', + revert: null, + ci: 'ci', + test: 'test', + }; + // Scopes for internal-only work, which release notes leave out + // (e.g. "fix(ci): ..." is labelled ci, not bug). + const scopeLabels = { ci: 'ci', test: 'test', tests: 'test' }; + const colors = { + 'breaking-change': 'b60205', + feature: '0e8a16', + bug: 'd73a4a', + performance: 'fbca04', + documentation: '0075ca', + maintenance: 'c5def5', + ci: 'ededed', + test: 'ededed', + }; + + // "type(optional scope)!: description" -> labels, or null if malformed. + function labelsFor(t) { + const m = /^([a-z]+)(?:\(([^)]+)\))?(!)?: \S/.exec(t || ''); + if (!m || !(m[1] in typeLabels)) return null; + const [, type, scope, bang] = m; + const labels = new Set(); + const label = (scope && scopeLabels[scope.toLowerCase()]) || typeLabels[type]; + if (label) labels.add(label); + if (bang) labels.add('breaking-change'); + return labels; + } + + const wanted = labelsFor(title); + if (!wanted) { + core.setFailed( + `PR title "${title}" must look like "type(scope): description" or ` + + `"type: description", where type is one of: ${Object.keys(typeLabels).join(', ')}. ` + + `Add "!" after the type or scope for a breaking change, e.g. "feat(plan)!: ...". ` + + `See CONTRIBUTING.md.` + ); + return; + } + + const current = context.payload.pull_request.labels.map((l) => l.name); + + // When the title is edited, drop the labels the old title produced. + // Labels added by people or by the path labeler are never removed. + const previous = labelsFor(context.payload.changes?.title?.from) || new Set(); + for (const name of previous) { + if (!wanted.has(name) && current.includes(name)) { + await github.rest.issues.removeLabel({ owner, repo, issue_number: pr, name }); + } + } + + const toAdd = [...wanted].filter((name) => !current.includes(name)); + for (const name of toAdd) { + try { + await github.rest.issues.getLabel({ owner, repo, name }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner, repo, name, color: colors[name] }); + } + } + if (toAdd.length) { + await github.rest.issues.addLabels({ owner, repo, issue_number: pr, labels: toAdd }); + } + + paths: + name: Label by changed files + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v6 + with: + configuration-path: .github/labeler.yml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 47fdd9ac7a..f4bdb7a928 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -69,3 +69,6 @@ jobs: with: generate_release_notes: true tag_name: ${{ github.ref_name }} + # Created as a draft so a maintainer can add highlights and upgrade + # notes before publishing. Publishing to PyPI does not wait for this. + draft: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4533069d5c..06899423aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,6 +87,25 @@ Optionally, `make install-pre-commit` installs git hooks so ruff and mypy run on - Include a DCO sign-off on all commits (`git commit -s`) - Be responsive to review feedback from maintainers +### Pull Request Titles + +Pull requests are squash-merged, so the title becomes the commit message and the +entry in the release notes. Titles must follow this format, which CI checks: + +``` +type(scope): description +``` + +- `type` is one of `feat`, `fix`, `perf`, `docs`, `refactor`, `chore`, `build`, `revert`, `ci` or `test`. +- `scope` is optional and names the area changed, e.g. `snowflake`, `plan`, `dbt` or `vscode`. +- Add `!` after the type or scope for a breaking change, e.g. `feat(plan)!: drop support for X`. +- Write the description for users: say what changed for them, not how the code changed. + +Release notes are grouped using labels that are applied automatically from the title +and the files changed. Use `ci`/`test` as the type or scope for internal-only changes +(e.g. `fix(ci): ...`) so they are left out of the release notes. A maintainer can add the +`skip-changelog` label to leave out any other PR. + ## Licensing Code contributions are licensed under the [Apache License 2.0](LICENSE). Documentation contributions are licensed under [Creative Commons Attribution 4.0 International (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/). See the LICENSE file and the [technical charter](sqlmesh-technical-charter.pdf) for details. From e9f3bde6e0b10ce6f763a13c290748354db89635 Mon Sep 17 00:00:00 2001 From: mday-io Date: Sun, 1 Nov 2026 21:47:23 +0000 Subject: [PATCH 2/6] ci: fix PR labeling issues found in review - Detect new state migrations with the pulls API: actions/labeler has no added-files option, and the invalid key would have failed every path label. - Treat the title as the source of truth for type labels, reading live labels instead of diffing against the previous title, and queue runs rather than cancelling them, so labels can't go stale. - Skip the title job on pushes, keep breaking changes out of the excluded ci/test labels, accept GitHub's Revert titles, avoid prototype lookups, and tolerate labels that were already removed. Signed-off-by: mday-io --- .github/labeler.yml | 6 +- .github/workflows/pr-labels.yaml | 145 +++++++++++++++++++------------ CONTRIBUTING.md | 11 ++- 3 files changed, 101 insertions(+), 61 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 7221450f1b..c782c856c7 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,8 +1,6 @@ # Path-based PR labels, applied by workflows/pr-labels.yaml. - -migration: - - changed-files: - - added-files: 'sqlmesh/migrations/v*.py' +# The migration label is applied by that workflow too, since labeler can't +# tell added files from modified ones. dbt: - changed-files: diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml index 87939e6227..02b2a18557 100644 --- a/.github/workflows/pr-labels.yaml +++ b/.github/workflows/pr-labels.yaml @@ -15,11 +15,15 @@ permissions: concurrency: group: pr-labels-${{ github.event.pull_request.number }} - cancel-in-progress: true + # Queue rather than cancel, so a run is never stopped part-way through + # changing labels. + cancel-in-progress: false jobs: title: name: Check title and label by type + # The title can't change on a push, so there is nothing to do. + if: github.event.action != 'synchronize' runs-on: ubuntu-latest steps: - uses: actions/github-script@v8 @@ -28,66 +32,78 @@ jobs: with: script: | const title = process.env.PR_TITLE; - const pr = context.payload.pull_request.number; + const issue_number = context.payload.pull_request.number; const { owner, repo } = context.repo; - const typeLabels = { - feat: 'feature', - fix: 'bug', - perf: 'performance', - docs: 'documentation', - refactor: 'maintenance', - chore: 'maintenance', - build: 'maintenance', - revert: null, - ci: 'ci', - test: 'test', - }; + const typeLabels = new Map([ + ['feat', 'feature'], + ['fix', 'bug'], + ['perf', 'performance'], + ['docs', 'documentation'], + ['refactor', 'maintenance'], + ['chore', 'maintenance'], + ['build', 'maintenance'], + ['revert', null], + ['ci', 'ci'], + ['test', 'test'], + ]); // Scopes for internal-only work, which release notes leave out // (e.g. "fix(ci): ..." is labelled ci, not bug). - const scopeLabels = { ci: 'ci', test: 'test', tests: 'test' }; - const colors = { - 'breaking-change': 'b60205', - feature: '0e8a16', - bug: 'd73a4a', - performance: 'fbca04', - documentation: '0075ca', - maintenance: 'c5def5', - ci: 'ededed', - test: 'ededed', - }; + const scopeLabels = new Map([['ci', 'ci'], ['test', 'test'], ['tests', 'test']]); + // Labels this job owns. The title is the source of truth for them: + // any that don't match the current title are removed. + const colors = new Map([ + ['breaking-change', 'b60205'], + ['feature', '0e8a16'], + ['bug', 'd73a4a'], + ['performance', 'fbca04'], + ['documentation', '0075ca'], + ['maintenance', 'c5def5'], + ['ci', 'ededed'], + ['test', 'ededed'], + ]); - // "type(optional scope)!: description" -> labels, or null if malformed. - function labelsFor(t) { - const m = /^([a-z]+)(?:\(([^)]+)\))?(!)?: \S/.exec(t || ''); - if (!m || !(m[1] in typeLabels)) return null; + let wanted; + if (/^Revert "/.test(title)) { + // Title generated by GitHub's "Revert" button. + wanted = new Set(); + } else { + const m = /^([a-z]+)(?:\(([^)]+)\))?(!)?: \S/.exec(title); + if (!m || !typeLabels.has(m[1])) { + core.setFailed( + `PR title "${title}" must look like "type(scope): description" or ` + + `"type: description", where type is lowercase and one of: ` + + `${[...typeLabels.keys()].join(', ')}. Add "!" after the type or scope ` + + `for a breaking change, e.g. "feat(plan)!: ...". See CONTRIBUTING.md. ` + + `Re-running this job checks the old title again: edit the title instead.` + ); + return; + } const [, type, scope, bang] = m; - const labels = new Set(); - const label = (scope && scopeLabels[scope.toLowerCase()]) || typeLabels[type]; - if (label) labels.add(label); - if (bang) labels.add('breaking-change'); - return labels; - } - - const wanted = labelsFor(title); - if (!wanted) { - core.setFailed( - `PR title "${title}" must look like "type(scope): description" or ` + - `"type: description", where type is one of: ${Object.keys(typeLabels).join(', ')}. ` + - `Add "!" after the type or scope for a breaking change, e.g. "feat(plan)!: ...". ` + - `See CONTRIBUTING.md.` - ); - return; + wanted = new Set(); + if (bang) { + // Breaking changes always appear in the release notes, so they + // never get the ci/test labels that exclude a PR. + wanted.add('breaking-change'); + const label = typeLabels.get(type); + if (label && label !== 'ci' && label !== 'test') wanted.add(label); + } else { + const label = (scope && scopeLabels.get(scope.toLowerCase())) || typeLabels.get(type); + if (label) wanted.add(label); + } } - const current = context.payload.pull_request.labels.map((l) => l.name); + const { data: live } = await github.rest.issues.listLabelsOnIssue({ + owner, repo, issue_number, per_page: 100, + }); + const current = live.map((l) => l.name); - // When the title is edited, drop the labels the old title produced. - // Labels added by people or by the path labeler are never removed. - const previous = labelsFor(context.payload.changes?.title?.from) || new Set(); - for (const name of previous) { - if (!wanted.has(name) && current.includes(name)) { - await github.rest.issues.removeLabel({ owner, repo, issue_number: pr, name }); + for (const name of current) { + if (!colors.has(name) || wanted.has(name)) continue; + try { + await github.rest.issues.removeLabel({ owner, repo, issue_number, name }); + } catch (e) { + if (e.status !== 404) throw e; } } @@ -97,11 +113,11 @@ jobs: await github.rest.issues.getLabel({ owner, repo, name }); } catch (e) { if (e.status !== 404) throw e; - await github.rest.issues.createLabel({ owner, repo, name, color: colors[name] }); + await github.rest.issues.createLabel({ owner, repo, name, color: colors.get(name) }); } } if (toAdd.length) { - await github.rest.issues.addLabels({ owner, repo, issue_number: pr, labels: toAdd }); + await github.rest.issues.addLabels({ owner, repo, issue_number, labels: toAdd }); } paths: @@ -111,3 +127,24 @@ jobs: - uses: actions/labeler@v6 with: configuration-path: .github/labeler.yml + + - name: Label new state migrations + uses: actions/github-script@v8 + with: + script: | + const issue_number = context.payload.pull_request.number; + const { owner, repo } = context.repo; + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, repo, pull_number: issue_number, per_page: 100, + }); + const added = files.some( + (f) => f.status === 'added' && /^sqlmesh\/migrations\/v\d+_.*\.py$/.test(f.filename) + ); + if (!added) return; + try { + await github.rest.issues.getLabel({ owner, repo, name: 'migration' }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner, repo, name: 'migration', color: '5319e7' }); + } + await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['migration'] }); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06899423aa..a5c4fbae96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,10 +101,15 @@ type(scope): description - Add `!` after the type or scope for a breaking change, e.g. `feat(plan)!: drop support for X`. - Write the description for users: say what changed for them, not how the code changed. +Titles created by GitHub's "Revert" button (`Revert "..."`) are also accepted. If the +title check fails, edit the title: re-running the failed job checks the old title again. + Release notes are grouped using labels that are applied automatically from the title -and the files changed. Use `ci`/`test` as the type or scope for internal-only changes -(e.g. `fix(ci): ...`) so they are left out of the release notes. A maintainer can add the -`skip-changelog` label to leave out any other PR. +and the files changed. The title decides the `feature`, `bug`, `performance`, +`documentation`, `maintenance`, `ci`, `test` and `breaking-change` labels, so to +recategorize a PR, change its title rather than its labels. Use `ci` or `test` as the type +or scope for internal-only changes (e.g. `fix(ci): ...`) so they are left out of the +release notes. A maintainer can add the `skip-changelog` label to leave out any other PR. ## Licensing From b9d60f032913dbfdb99a4e9c28e6a3eded2a9278 Mon Sep 17 00:00:00 2001 From: mday-io Date: Sun, 1 Nov 2026 22:10:27 +0000 Subject: [PATCH 3/6] ci: use sentence-case release note section titles Match the section names that RELEASING.md (SQLMesh/sqlmesh#6180) uses. Signed-off-by: mday-io --- .github/release.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/release.yml b/.github/release.yml index c389bb2b4d..e1df0c159d 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -2,6 +2,7 @@ # `generate_release_notes` in workflows/release.yaml) by PR label. # Labels are applied automatically by workflows/pr-labels.yaml. # A PR is listed under the first category whose labels it matches. +# RELEASING.md refers to these section titles, so keep the two in sync. changelog: exclude: labels: @@ -10,27 +11,27 @@ changelog: - test - dependencies categories: - - title: ⚠️ Breaking Changes + - title: Breaking changes labels: - breaking-change - - title: 🗄️ State Migrations (run `sqlmesh migrate` after upgrading) + - title: State migrations (run `sqlmesh migrate` after upgrading) labels: - migration - - title: 🚀 Features + - title: Features labels: - feature - - title: ⚡ Performance + - title: Performance labels: - performance - - title: 🐛 Bug Fixes + - title: Bug fixes labels: - bug - - title: 📚 Documentation + - title: Documentation labels: - documentation - - title: 🧰 Maintenance + - title: Maintenance labels: - maintenance - - title: Other Changes + - title: Other changes labels: - "*" From 031268160b803a9d9cba7222ac5bbb25d9ea4e4b Mon Sep 17 00:00:00 2001 From: mday-io Date: Sun, 1 Nov 2026 22:20:52 +0000 Subject: [PATCH 4/6] ci: trim to release-note grouping and add release docs Narrow the PR to grouping release notes by label: - Drop the path labeler. Its engine and area labels don't feed any release-note section. - Drop the Release Note section from the PR template. Nothing reads it. - Keep creating published GitHub releases rather than drafts. Whether to switch to drafts is a process decision for maintainers. Add RELEASING.md with the steps for releasing the Python package, the VS Code extension and the shared web package, and link to it from CONTRIBUTING.md. It was previously proposed separately in SQLMesh/sqlmesh#6180. Signed-off-by: mday-io --- .github/labeler.yml | 83 -------------------------------- .github/pull_request_template.md | 9 ---- .github/workflows/pr-labels.yaml | 11 ++--- .github/workflows/release.yaml | 3 -- CONTRIBUTING.md | 4 ++ RELEASING.md | 66 +++++++++++++++++++++++++ 6 files changed, 73 insertions(+), 103 deletions(-) delete mode 100644 .github/labeler.yml create mode 100644 RELEASING.md diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index c782c856c7..0000000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,83 +0,0 @@ -# Path-based PR labels, applied by workflows/pr-labels.yaml. -# The migration label is applied by that workflow too, since labeler can't -# tell added files from modified ones. - -dbt: - - changed-files: - - any-glob-to-any-file: ['sqlmesh/dbt/**', 'sqlmesh_dbt/**'] - -vscode: - - changed-files: - - any-glob-to-any-file: ['vscode/**', 'sqlmesh/lsp/**'] - -web-ui: - - changed-files: - - any-glob-to-any-file: ['web/**'] - -cicd-bot: - - changed-files: - - any-glob-to-any-file: ['sqlmesh/integrations/github/**', 'sqlmesh/cicd/**'] - -engine:athena: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/athena.py' - -engine:bigquery: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/bigquery.py' - -engine:clickhouse: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/clickhouse.py' - -engine:databricks: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/databricks.py' - -engine:db2: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/db2.py' - -engine:duckdb: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/duckdb.py' - -engine:fabric: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/fabric.py' - -engine:mssql: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/mssql.py' - -engine:mysql: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/mysql.py' - -engine:postgres: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/postgres.py' - -engine:redshift: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/redshift.py' - -engine:risingwave: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/risingwave.py' - -engine:snowflake: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/snowflake.py' - -engine:spark: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/spark.py' - -engine:starrocks: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/starrocks.py' - -engine:trino: - - changed-files: - - any-glob-to-any-file: 'sqlmesh/core/engine_adapter/trino.py' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7517edf6ef..7585f0ce10 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,21 +2,12 @@ -## Release Note - - - ## Test Plan ## Checklist -- [ ] My PR title follows the `type(scope): description` format (see [CONTRIBUTING.md](CONTRIBUTING.md#pull-request-titles)) - [ ] I have run `make style` and fixed any issues - [ ] I have added tests for my changes (if applicable) - [ ] All existing tests pass (`make fast-test`) diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml index 02b2a18557..aaa3662a3d 100644 --- a/.github/workflows/pr-labels.yaml +++ b/.github/workflows/pr-labels.yaml @@ -120,16 +120,11 @@ jobs: await github.rest.issues.addLabels({ owner, repo, issue_number, labels: toAdd }); } - paths: - name: Label by changed files + migrations: + name: Label new state migrations runs-on: ubuntu-latest steps: - - uses: actions/labeler@v6 - with: - configuration-path: .github/labeler.yml - - - name: Label new state migrations - uses: actions/github-script@v8 + - uses: actions/github-script@v8 with: script: | const issue_number = context.payload.pull_request.number; diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f4bdb7a928..47fdd9ac7a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -69,6 +69,3 @@ jobs: with: generate_release_notes: true tag_name: ${{ github.ref_name }} - # Created as a draft so a maintainer can add highlights and upgrade - # notes before publishing. Publishing to PyPI does not wait for this. - draft: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5c4fbae96..3041922db9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,6 +111,10 @@ recategorize a PR, change its title rather than its labels. Use `ci` or `test` a or scope for internal-only changes (e.g. `fix(ci): ...`) so they are left out of the release notes. A maintainer can add the `skip-changelog` label to leave out any other PR. +## Releases + +Maintainers publish releases. See [RELEASING.md](RELEASING.md) for the steps. + ## Licensing Code contributions are licensed under the [Apache License 2.0](LICENSE). Documentation contributions are licensed under [Creative Commons Attribution 4.0 International (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/). See the LICENSE file and the [technical charter](sqlmesh-technical-charter.pdf) for details. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000000..606f0de7b4 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,66 @@ +# Releasing SQLMesh + +This guide is for maintainers with write access to `SQLMesh/sqlmesh`. It covers the three things the project publishes: + +| Package | Registry | How a release starts | +|---|---|---| +| `sqlmesh` (Python) | PyPI | Push a `v*.*.*` tag | +| SQLMesh VS Code extension | VS Code Marketplace and Open VSX | Run the **Release VSCode Extension** workflow | +| `@tobikodata/sqlmesh-common` | npm | Run the **Release web common code** workflow | + +## Release the Python Package + +The [`Release`](.github/workflows/release.yaml) workflow runs when a tag that matches `v*.*.*` is pushed. The package version comes from the tag through `setuptools_scm`, so you don't edit a version number in the code. + +The workflow does four things: + +1. Builds the web UI and bundles it into the package. +2. Publishes `sqlmesh` to PyPI with `make publish`. +3. Publishes the test package to the private `tobiko-private` registry with `make publish-tests`. +4. Creates a GitHub release with notes generated from the pull requests merged since the previous tag. + +Each step publishes immediately and there's no approval gate. Check `main` before you tag. + +### Steps + +1. Pick the commit to release. It must be on `main`, and every required check must pass on it. Check the latest **pr.yaml** run for that commit on the [Actions page](https://github.com/SQLMesh/sqlmesh/actions/workflows/pr.yaml). +2. Pick the version. Find the latest tag with `git tag --sort=-creatordate | head -1` and increment it. +3. Tag the commit and push the tag: + + ```bash + git fetch main --tags + git tag v0.236.5 + git push v0.236.5 + ``` + + Replace `` with your remote for `SQLMesh/sqlmesh`. Push only the tag, not a branch. + +4. Watch the [Release](https://github.com/SQLMesh/sqlmesh/actions/workflows/release.yaml) run until it finishes. +5. Check that the new version is on [PyPI](https://pypi.org/project/sqlmesh/#history). +6. Edit the generated release on the [Releases page](https://github.com/SQLMesh/sqlmesh/releases). Under **Breaking changes**, add a short upgrade note for each entry. Say what users will see after upgrading, such as a migration or a metadata-only change in `sqlmesh plan`. + +If the workflow fails after the PyPI step, don't delete or reuse the tag. PyPI doesn't accept the same version twice. Fix the cause and release the next version. + +### Release Notes + +GitHub builds the notes from merged pull request titles and groups them by label, as configured in [`.github/release.yml`](.github/release.yml). The [PR Labels](.github/workflows/pr-labels.yaml) workflow applies the labels from each pull request's title (`type(scope): description`), and adds `migration` to pull requests that add a state migration. See [Pull Request Titles](CONTRIBUTING.md#pull-request-titles) in `CONTRIBUTING.md`. + +- A `!` in the title, such as `fix!:` or `feat!:`, puts the pull request under **Breaking changes**. +- A pull request that adds a state migration goes under **State migrations**. +- CI, test and Dependabot pull requests are left out. Add the `skip-changelog` label to leave out any other pull request. + +To move a pull request to another section, change its title. The workflow resets the type labels from the title. + +## Release the VS Code Extension + +The [Release VSCode Extension](.github/workflows/release_extension.yaml) workflow publishes to the VS Code Marketplace and to Open VSX. + +1. On the workflow's page, choose **Run workflow**. +2. Select the `main` branch. The workflow stops if you run it from any other branch. +3. Enter the version without a `v`, for example `0.0.8`. + +The workflow sets the version only in its own build. It doesn't commit it, so `vscode/extension/package.json` keeps its old version. Check the [Marketplace listing](https://marketplace.visualstudio.com/items?itemName=tobikodata.sqlmesh) for the latest published version and enter a higher one. + +## Release the Shared Web Package + +The [Release web common code](.github/workflows/release_shared_js.yaml) workflow publishes `@tobikodata/sqlmesh-common` from `web/common` to npm. Run it the same way as the extension workflow: from `main`, with a version that has no `v`. It also doesn't commit the version, so check [npm](https://www.npmjs.com/package/@tobikodata/sqlmesh-common) for the latest published version first. From 5758b3d800b003621d02a18dacfe49d78889a64d Mon Sep 17 00:00:00 2001 From: mday-io Date: Sun, 1 Nov 2026 22:26:52 +0000 Subject: [PATCH 5/6] ci: address review feedback on PR labels and release steps - Run the title job on pushes too. GitHub keeps one pending run per concurrency group, so a push could replace a queued run for a title edit and leave labels for the old title. - Remove the migration label when a PR no longer adds a migration. - RELEASING.md: fetch tags before picking the version, sort tags by version rather than date, and use a VERSION variable instead of a hard-coded tag in the tag and push commands. - RELEASING.md: note that a PR appears in only one release-note section, so a breaking change with a migration is listed under Breaking changes. Signed-off-by: mday-io --- .github/workflows/pr-labels.yaml | 18 +++++++++++++++--- RELEASING.md | 20 +++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml index aaa3662a3d..46fe7167d6 100644 --- a/.github/workflows/pr-labels.yaml +++ b/.github/workflows/pr-labels.yaml @@ -22,8 +22,8 @@ concurrency: jobs: title: name: Check title and label by type - # The title can't change on a push, so there is nothing to do. - if: github.event.action != 'synchronize' + # Runs on pushes too: GitHub keeps only one pending run per concurrency + # group, so a push can replace a queued run for a title edit. runs-on: ubuntu-latest steps: - uses: actions/github-script@v8 @@ -135,7 +135,19 @@ jobs: const added = files.some( (f) => f.status === 'added' && /^sqlmesh\/migrations\/v\d+_.*\.py$/.test(f.filename) ); - if (!added) return; + const labels = context.payload.pull_request.labels.map((l) => l.name); + if (!added) { + // The PR no longer adds a migration, e.g. the file was removed. + if (labels.includes('migration')) { + try { + await github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'migration' }); + } catch (e) { + if (e.status !== 404) throw e; + } + } + return; + } + if (labels.includes('migration')) return; try { await github.rest.issues.getLabel({ owner, repo, name: 'migration' }); } catch (e) { diff --git a/RELEASING.md b/RELEASING.md index 606f0de7b4..5b92e257f5 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -24,16 +24,24 @@ Each step publishes immediately and there's no approval gate. Check `main` befor ### Steps 1. Pick the commit to release. It must be on `main`, and every required check must pass on it. Check the latest **pr.yaml** run for that commit on the [Actions page](https://github.com/SQLMesh/sqlmesh/actions/workflows/pr.yaml). -2. Pick the version. Find the latest tag with `git tag --sort=-creatordate | head -1` and increment it. -3. Tag the commit and push the tag: +2. Pick the version. Fetch the tags, then find the highest released version and increment it: ```bash git fetch main --tags - git tag v0.236.5 - git push v0.236.5 + git tag --list 'v[0-9]*' --sort=-v:refname | head -1 ``` - Replace `` with your remote for `SQLMesh/sqlmesh`. Push only the tag, not a branch. + Replace `` with your remote for `SQLMesh/sqlmesh`. + +3. Tag the commit and push the tag. Set `VERSION` to the version from step 2, for example `v0.236.5`: + + ```bash + VERSION=v0.236.5 + git tag "$VERSION" + git push "$VERSION" + ``` + + Push only the tag, not a branch. 4. Watch the [Release](https://github.com/SQLMesh/sqlmesh/actions/workflows/release.yaml) run until it finishes. 5. Check that the new version is on [PyPI](https://pypi.org/project/sqlmesh/#history). @@ -49,6 +57,8 @@ GitHub builds the notes from merged pull request titles and groups them by label - A pull request that adds a state migration goes under **State migrations**. - CI, test and Dependabot pull requests are left out. Add the `skip-changelog` label to leave out any other pull request. +Each pull request appears in only one section: the first one in this list that matches its labels. A breaking change that also adds a state migration is listed under **Breaking changes**, so its upgrade note in step 6 should say to run `sqlmesh migrate`. + To move a pull request to another section, change its title. The workflow resets the type labels from the title. ## Release the VS Code Extension From a36f769f2ab705bf3988f624c6b79c711b9dbeb4 Mon Sep 17 00:00:00 2001 From: mday-io Date: Tue, 3 Nov 2026 16:53:40 +0000 Subject: [PATCH 6/6] ci: read live labels in the migration label job A queued run's event payload can hold labels from before an earlier run changed them. Read the PR's current labels instead, as the title job does. Signed-off-by: mday-io --- .github/workflows/pr-labels.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml index 46fe7167d6..b7a000d10b 100644 --- a/.github/workflows/pr-labels.yaml +++ b/.github/workflows/pr-labels.yaml @@ -135,7 +135,11 @@ jobs: const added = files.some( (f) => f.status === 'added' && /^sqlmesh\/migrations\/v\d+_.*\.py$/.test(f.filename) ); - const labels = context.payload.pull_request.labels.map((l) => l.name); + // Read live labels: a queued run's event payload can be out of date. + const { data: live } = await github.rest.issues.listLabelsOnIssue({ + owner, repo, issue_number, per_page: 100, + }); + const labels = live.map((l) => l.name); if (!added) { // The PR no longer adds a migration, e.g. the file was removed. if (labels.includes('migration')) {