diff --git a/.github/scripts/nuget-release.cjs b/.github/scripts/nuget-release.cjs new file mode 100644 index 0000000..fd829c6 --- /dev/null +++ b/.github/scripts/nuget-release.cjs @@ -0,0 +1,70 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +function requireMainPush(context) { + if (context.eventName !== 'push' || context.ref !== 'refs/heads/main') { + throw new Error('Publishing is only allowed for a push to main.'); + } +} + +async function findTag({ github, context, tag }) { + try { + const { data } = await github.rest.git.getRef({ ...context.repo, ref: `tags/${tag}` }); + if (data.object.type !== 'commit' || data.object.sha !== context.sha) { + throw new Error(`Tag ${tag} does not point to the tested main commit.`); + } + return data; + } catch (error) { + if (error.status === 404) return null; + throw error; + } +} + +async function reserveTag(options) { + const { github, context, tag } = options; + requireMainPush(context); + if (!await findTag(options)) { + await github.rest.git.createRef({ + ...context.repo, ref: `refs/tags/${tag}`, sha: context.sha + }); + } +} + +async function publishRelease(options) { + const { github, context, tag, packageId, version, artifacts = 'artifacts' } = options; + requireMainPush(context); + if (!await findTag(options)) throw new Error('The release tag must be reserved before publishing.'); + const name = `${packageId}.${version}.nupkg`; + const data = fs.readFileSync(path.join(artifacts, name)); + let release; + try { + ({ data: release } = await github.rest.repos.getReleaseByTag({ ...context.repo, tag })); + } catch (error) { + if (error.status !== 404) throw error; + ({ data: release } = await github.rest.repos.createRelease({ + ...context.repo, tag_name: tag, target_commitish: context.sha, + name: `${packageId} ${version}`, draft: true, prerelease: false, + generate_release_notes: true + })); + } + const assets = await github.paginate(github.rest.repos.listReleaseAssets, { + ...context.repo, release_id: release.id, per_page: 100 + }); + const asset = assets.find(item => item.name === name); + if (asset && asset.state !== 'uploaded') { + await github.rest.repos.deleteReleaseAsset({ ...context.repo, asset_id: asset.id }); + } + if (!asset || asset.state !== 'uploaded') { + await github.rest.repos.uploadReleaseAsset({ + ...context.repo, release_id: release.id, name, data, + headers: { 'content-type': 'application/octet-stream', 'content-length': data.length } + }); + } + if (release.draft) { + await github.rest.repos.updateRelease({ + ...context.repo, release_id: release.id, draft: false, make_latest: 'false' + }); + } +} + +module.exports = { requireMainPush, reserveTag, publishRelease }; diff --git a/.github/scripts/nuget-release.test.cjs b/.github/scripts/nuget-release.test.cjs new file mode 100644 index 0000000..14f17df --- /dev/null +++ b/.github/scripts/nuget-release.test.cjs @@ -0,0 +1,122 @@ +const { test, after } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { reserveTag, publishRelease } = require('./nuget-release.cjs'); + +const artifacts = fs.mkdtempSync(path.join(os.tmpdir(), 'syncsql-nuget-release-')); +after(() => fs.rmSync(artifacts, { recursive: true })); +fs.writeFileSync(path.join(artifacts, 'SyncSql.Cli.2026.9.26.42.nupkg'), 'package bytes'); + +function fixture() { + const calls = []; + const state = { tag: null, release: null, assets: [] }; + const missing = () => { throw Object.assign(new Error('Not found'), { status: 404 }); }; + const github = { + rest: { + git: { + getRef: async () => state.tag ? { data: state.tag } : missing(), + createRef: async args => { + calls.push(['tag', args]); + state.tag = { object: { type: 'commit', sha: args.sha } }; + } + }, + repos: { + getReleaseByTag: async () => state.release ? { data: state.release } : missing(), + createRelease: async args => { + calls.push(['draft', args]); + state.release = { id: 7, draft: true }; + return { data: state.release }; + }, + listReleaseAssets: async () => state.assets, + deleteReleaseAsset: async args => { calls.push(['delete', args]); state.assets = []; }, + uploadReleaseAsset: async args => { + calls.push(['upload', args]); + state.assets.push({ id: 8, name: args.name, state: 'uploaded' }); + }, + updateRelease: async args => { + calls.push(['publish', args]); + state.release.draft = args.draft; + } + } + }, + paginate: async method => method() + }; + return { + calls, state, github, artifacts, tag: 'cli-v2026.9.26.42', + packageId: 'SyncSql.Cli', version: '2026.9.26.42', + context: { eventName: 'push', ref: 'refs/heads/main', sha: 'tested-sha', repo: { owner: 'owner', repo: 'repo' } } + }; +} + +for (const [eventName, ref] of [ + ['pull_request', 'refs/pull/69/merge'], ['workflow_dispatch', 'refs/heads/main'], + ['push', 'refs/heads/feature'], ['push', 'refs/tags/cli-v2026.9.26.42'] +]) { + test(`rejects ${eventName} on ${ref} before writing anything`, async () => { + const f = fixture(); + Object.assign(f.context, { eventName, ref }); + await assert.rejects(reserveTag(f), /only allowed/); + await assert.rejects(publishRelease(f), /only allowed/); + assert.deepEqual(f.calls, []); + }); +} + +test('tags the tested commit, uploads to a draft, then publishes; reruns are idempotent', async () => { + const f = fixture(); + await reserveTag(f); + await publishRelease(f); + assert.deepEqual(f.calls.map(([name]) => name), ['tag', 'draft', 'upload', 'publish']); + assert.equal(f.calls[0][1].sha, 'tested-sha'); + assert.equal(f.calls[1][1].target_commitish, 'tested-sha'); + assert.equal(f.calls[1][1].generate_release_notes, true); + assert.equal(f.calls[2][1].data.toString(), 'package bytes'); + assert.equal(f.calls[3][1].draft, false); + await reserveTag(f); + await publishRelease(f); + assert.equal(f.calls.length, 4); +}); + +test('rejects a tag pointing elsewhere rather than moving it', async () => { + const f = fixture(); + f.state.tag = { object: { type: 'commit', sha: 'different-sha' } }; + await assert.rejects(reserveTag(f), /tested main commit/); + await assert.rejects(publishRelease(f), /tested main commit/); + assert.deepEqual(f.calls, []); +}); + +test('requires a reserved tag before creating a release', async () => { + const f = fixture(); + await assert.rejects(publishRelease(f), /must be reserved/); + assert.deepEqual(f.calls, []); +}); + +test('propagates API authorization errors instead of treating them as missing tags', async () => { + const f = fixture(); + f.github.rest.git.getRef = async () => { throw Object.assign(new Error('Forbidden'), { status: 403 }); }; + await assert.rejects(reserveTag(f), /Forbidden/); + assert.deepEqual(f.calls, []); +}); + +test('an upload failure leaves a draft that can be completed on retry', async () => { + const f = fixture(); + await reserveTag(f); + const upload = f.github.rest.repos.uploadReleaseAsset; + f.github.rest.repos.uploadReleaseAsset = async () => { throw new Error('Upload failed'); }; + await assert.rejects(publishRelease(f), /Upload failed/); + assert.equal(f.state.release.draft, true); + f.github.rest.repos.uploadReleaseAsset = upload; + await publishRelease(f); + assert.equal(f.state.release.draft, false); + assert.equal(f.calls.filter(([name]) => name === 'draft').length, 1); +}); + +test('replaces an incomplete asset before publishing a recovered draft', async () => { + const f = fixture(); + await reserveTag(f); + f.state.release = { id: 7, draft: true }; + f.state.assets = [{ id: 8, name: 'SyncSql.Cli.2026.9.26.42.nupkg', state: 'starter' }]; + await publishRelease(f); + assert.deepEqual(f.calls.map(([name]) => name), ['tag', 'delete', 'upload', 'publish']); +}); diff --git a/.github/workflows/cli-publish-nuget.yml b/.github/workflows/cli-publish-nuget.yml new file mode 100644 index 0000000..b8820e3 --- /dev/null +++ b/.github/workflows/cli-publish-nuget.yml @@ -0,0 +1,122 @@ +name: Publish SyncSql.Cli to NuGet + +on: + push: + branches: [main] + paths: + - 'cli/**' + - 'grammar/**' + - 'global.json' + - '.github/workflows/cli-publish-nuget.yml' + - '.github/scripts/nuget-release*.cjs' + pull_request: + paths: + - 'cli/**' + - 'grammar/**' + - 'global.json' + - '.github/workflows/cli-publish-nuget.yml' + - '.github/scripts/nuget-release*.cjs' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: nuget-cli-${{ github.ref }} + cancel-in-progress: false + +jobs: + package: + runs-on: ubuntu-latest + timeout-minutes: 30 + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - name: Test release automation + run: node --test .github/scripts/nuget-release.test.cjs + - name: Resolve CalVer + id: version + shell: pwsh + run: | + $timestamp = git show -s --format=%cI $env:GITHUB_SHA + if ($LASTEXITCODE -ne 0) { throw 'Cannot read the build commit date.' } + $date = [DateTimeOffset]::Parse($timestamp, [Globalization.CultureInfo]::InvariantCulture).UtcDateTime + $revision = [int]$env:GITHUB_RUN_NUMBER + if ($revision -lt 1 -or $revision -gt 65534) { throw 'Run number must be between 1 and 65534 for .NET assembly compatibility.' } + $version = $date.ToString('yyyy.M.d', [Globalization.CultureInfo]::InvariantCulture) + ".$revision" + if ($env:GITHUB_EVENT_NAME -ne 'push' -or $env:GITHUB_REF -ne 'refs/heads/main') { + $version += "-ci.$env:GITHUB_RUN_ATTEMPT" + } + "PACKAGE_VERSION=$version" >> $env:GITHUB_ENV + "version=$version" >> $env:GITHUB_OUTPUT + "tag=cli-v$version" >> $env:GITHUB_OUTPUT + - name: Test + run: dotnet test cli/SyncSql.slnx --configuration Release -p:ContinuousIntegrationBuild=true + - name: Pack + run: >- + dotnet pack cli/src/SyncSql.Cli/SyncSql.Cli.csproj + --configuration Release --output artifacts + -p:Version="$PACKAGE_VERSION" -p:ContinuousIntegrationBuild=true + - name: Smoke test the packaged tool + run: | + dotnet tool install SyncSql.Cli --tool-path "$RUNNER_TEMP/syncsql-tool" --add-source "$PWD/artifacts" --version "$PACKAGE_VERSION" + "$RUNNER_TEMP/syncsql-tool/syncsql" --help + - uses: actions/upload-artifact@v4 + with: + name: SyncSql.Cli-nuget + path: artifacts/SyncSql.Cli.*.nupkg + if-no-files-found: error + + publish: + needs: package + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + id-token: write + env: + RELEASE_TAG: ${{ needs.package.outputs.tag }} + PACKAGE_VERSION: ${{ needs.package.outputs.version }} + PACKAGE_ID: SyncSql.Cli + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - uses: actions/download-artifact@v4 + with: + name: SyncSql.Cli-nuget + path: artifacts + - name: Reserve release tag at the tested main commit + uses: actions/github-script@v7 + with: + script: | + const { reserveTag } = require('./.github/scripts/nuget-release.cjs'); + await reserveTag({ github, context, tag: process.env.RELEASE_TAG }); + - name: Authenticate to NuGet + uses: NuGet/login@v1 + id: login + with: + user: ${{ secrets.NUGET_USER }} + - name: Publish package + env: + NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} + run: >- + dotnet nuget push "artifacts/SyncSql.Cli.$PACKAGE_VERSION.nupkg" + --source https://api.nuget.org/v3/index.json + --api-key "$NUGET_API_KEY" --skip-duplicate + - name: Create GitHub release with the published package + uses: actions/github-script@v7 + with: + script: | + const { publishRelease } = require('./.github/scripts/nuget-release.cjs'); + await publishRelease({ + github, context, tag: process.env.RELEASE_TAG, + packageId: process.env.PACKAGE_ID, version: process.env.PACKAGE_VERSION + }); diff --git a/.github/workflows/grammar-publish-nuget.yml b/.github/workflows/grammar-publish-nuget.yml new file mode 100644 index 0000000..dbd13d4 --- /dev/null +++ b/.github/workflows/grammar-publish-nuget.yml @@ -0,0 +1,126 @@ +name: Publish SyncSql.Grammar.PlSql to NuGet + +on: + push: + branches: [main] + paths: + - 'grammar/**' + - 'cli/src/SyncSql.Grammar.PlSql/**' + - 'cli/src/SyncSql.Lineage.Oracle/**' + - 'cli/src/SyncSql.Core/**' + - 'cli/tests/SyncSql.Lineage.Oracle.Tests/**' + - 'cli/Directory.Build.props' + - 'global.json' + - '.github/workflows/grammar-publish-nuget.yml' + - '.github/scripts/nuget-release*.cjs' + pull_request: + paths: + - 'grammar/**' + - 'cli/src/SyncSql.Grammar.PlSql/**' + - 'cli/src/SyncSql.Lineage.Oracle/**' + - 'cli/src/SyncSql.Core/**' + - 'cli/tests/SyncSql.Lineage.Oracle.Tests/**' + - 'cli/Directory.Build.props' + - 'global.json' + - '.github/workflows/grammar-publish-nuget.yml' + - '.github/scripts/nuget-release*.cjs' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: nuget-grammar-${{ github.ref }} + cancel-in-progress: false + +jobs: + package: + runs-on: ubuntu-latest + timeout-minutes: 30 + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - name: Test release automation + run: node --test .github/scripts/nuget-release.test.cjs + - name: Resolve CalVer + id: version + shell: pwsh + run: | + $timestamp = git show -s --format=%cI $env:GITHUB_SHA + if ($LASTEXITCODE -ne 0) { throw 'Cannot read the build commit date.' } + $date = [DateTimeOffset]::Parse($timestamp, [Globalization.CultureInfo]::InvariantCulture).UtcDateTime + $revision = [int]$env:GITHUB_RUN_NUMBER + if ($revision -lt 1 -or $revision -gt 65534) { throw 'Run number must be between 1 and 65534 for .NET assembly compatibility.' } + $version = $date.ToString('yyyy.M.d', [Globalization.CultureInfo]::InvariantCulture) + ".$revision" + if ($env:GITHUB_EVENT_NAME -ne 'push' -or $env:GITHUB_REF -ne 'refs/heads/main') { + $version += "-ci.$env:GITHUB_RUN_ATTEMPT" + } + "PACKAGE_VERSION=$version" >> $env:GITHUB_ENV + "version=$version" >> $env:GITHUB_OUTPUT + "tag=grammar-v$version" >> $env:GITHUB_OUTPUT + - name: Test + run: dotnet test cli/tests/SyncSql.Lineage.Oracle.Tests/SyncSql.Lineage.Oracle.Tests.csproj --configuration Release -p:ContinuousIntegrationBuild=true + - name: Pack + run: >- + dotnet pack cli/src/SyncSql.Grammar.PlSql/SyncSql.Grammar.PlSql.csproj + --configuration Release --output artifacts + -p:Version="$PACKAGE_VERSION" -p:ContinuousIntegrationBuild=true + - uses: actions/upload-artifact@v4 + with: + name: SyncSql.Grammar.PlSql-nuget + path: artifacts/SyncSql.Grammar.PlSql.*.nupkg + if-no-files-found: error + + publish: + needs: package + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + id-token: write + env: + RELEASE_TAG: ${{ needs.package.outputs.tag }} + PACKAGE_VERSION: ${{ needs.package.outputs.version }} + PACKAGE_ID: SyncSql.Grammar.PlSql + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - uses: actions/download-artifact@v4 + with: + name: SyncSql.Grammar.PlSql-nuget + path: artifacts + - name: Reserve release tag at the tested main commit + uses: actions/github-script@v7 + with: + script: | + const { reserveTag } = require('./.github/scripts/nuget-release.cjs'); + await reserveTag({ github, context, tag: process.env.RELEASE_TAG }); + - name: Authenticate to NuGet + uses: NuGet/login@v1 + id: login + with: + user: ${{ secrets.NUGET_USER }} + - name: Publish package + env: + NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} + run: >- + dotnet nuget push "artifacts/SyncSql.Grammar.PlSql.$PACKAGE_VERSION.nupkg" + --source https://api.nuget.org/v3/index.json + --api-key "$NUGET_API_KEY" --skip-duplicate + - name: Create GitHub release with the published package + uses: actions/github-script@v7 + with: + script: | + const { publishRelease } = require('./.github/scripts/nuget-release.cjs'); + await publishRelease({ + github, context, tag: process.env.RELEASE_TAG, + packageId: process.env.PACKAGE_ID, version: process.env.PACKAGE_VERSION + }); diff --git a/cli/Directory.Build.props b/cli/Directory.Build.props index cd51e78..f9133e9 100644 --- a/cli/Directory.Build.props +++ b/cli/Directory.Build.props @@ -20,7 +20,8 @@ SyncSQL SyncSQL - 1.0.0 + + $([System.DateTime]::UtcNow.ToString('yyyy.M.d'))-dev https://github.com/cangelosilima/SyncSQL diff --git a/cli/docs/cli.md b/cli/docs/cli.md index 6415d23..af3bf25 100644 --- a/cli/docs/cli.md +++ b/cli/docs/cli.md @@ -45,14 +45,14 @@ Windows authentication can instead use the identity running the CLI. `syncsql` is published as a [dotnet global tool](https://learn.microsoft.com/dotnet/core/tools/global-tools). ```bash -dotnet tool install --global SyncSql.Cli --add-source +dotnet tool install --global SyncSql.Cli ``` Once installed, the `syncsql` command is on your `PATH` (dotnet prints the exact line to add if it isn't already). Upgrade with: ```bash -dotnet tool update --global SyncSql.Cli --add-source +dotnet tool update --global SyncSql.Cli ``` Requires the [.NET 10 runtime](https://dotnet.microsoft.com/download) (or @@ -62,10 +62,14 @@ driver. ### Building and installing from source +Merges into `main` publish to nuget.org and create independent `cli-v*` GitHub releases. See +[NuGet publishing](../../docs/nuget-publishing.md) for authentication setup and +the separate grammar release workflow. + ```bash cd cli dotnet pack src/SyncSql.Cli -c Release -dotnet tool install --global --add-source ./nupkg SyncSql.Cli +dotnet tool install --global --add-source ./nupkg --prerelease SyncSql.Cli ``` #### Build prerequisites diff --git a/cli/src/SyncSql.Cli/SyncSql.Cli.csproj b/cli/src/SyncSql.Cli/SyncSql.Cli.csproj index 379ecfc..66b6614 100644 --- a/cli/src/SyncSql.Cli/SyncSql.Cli.csproj +++ b/cli/src/SyncSql.Cli/SyncSql.Cli.csproj @@ -32,8 +32,16 @@ true syncsql SyncSql.Cli + README.md + git Extraction, lineage, and catalog building for a fleet of MSSQL/Oracle servers - the syncsql CLI. $(MSBuildThisFileDirectory)../../nupkg + + + + + + diff --git a/cli/src/SyncSql.Grammar.PlSql/SyncSql.Grammar.PlSql.csproj b/cli/src/SyncSql.Grammar.PlSql/SyncSql.Grammar.PlSql.csproj index 49c201e..705b8c8 100644 --- a/cli/src/SyncSql.Grammar.PlSql/SyncSql.Grammar.PlSql.csproj +++ b/cli/src/SyncSql.Grammar.PlSql/SyncSql.Grammar.PlSql.csproj @@ -10,7 +10,13 @@ false false false - false + true + SyncSql.Grammar.PlSql + Oracle SQL and PL/SQL lexer, parser, and visitor generated with ANTLR 4.13.1. Targets .NET Standard 2.0. + Apache-2.0 + README.md + git + oracle;sql;plsql;antlr;parser;grammar $(NoWarn);CS3021 @@ -35,6 +41,9 @@ + + + diff --git a/docs/nuget-publishing.md b/docs/nuget-publishing.md new file mode 100644 index 0000000..53b87a3 --- /dev/null +++ b/docs/nuget-publishing.md @@ -0,0 +1,87 @@ +# Publishing NuGet packages + +The grammar and CLI publish independently when relevant changes are merged into +`main`. Each workflow tests and packages its component, publishes it to nuget.org, +and creates a GitHub release with generated notes and the `.nupkg` attached. + +| Package | Workflow | Example generated release tag | +| --- | --- | --- | +| `SyncSql.Grammar.PlSql` | `grammar-publish-nuget.yml` | `grammar-v2026.9.26.42` | +| `SyncSql.Cli` (.NET tool, command `syncsql`) | `cli-publish-nuget.yml` | `cli-v2026.9.26.57` | + +## CalVer + +Versions use **`YYYY.M.D.BUILD`**, without leading zeros. The date is the tested +main commit's UTC committer date, and BUILD is the workflow's run number. Each +workflow has its own sequence; validation runs can leave gaps. BUILD must be +between 1 and 65534 for .NET assembly compatibility. + +The date and run number stay the same when a failed run is rerun, even on another +day. The generated version overrides `cli/Directory.Build.props`; local builds +default to the current UTC date followed by `-dev`. + +Publishing the CLI does not require publishing the grammar first: the tool +bundles the grammar assembly through its project reference. The standalone +grammar targets .NET Standard 2.0 and declares its ANTLR runtime dependency. + +## One-time setup + +Before merging the workflows: + +1. Set the Actions repository secret `NUGET_USER` to the NuGet profile username + (currently `this.programmer`, not an email address). +2. In nuget.org, configure two + [trusted publishing policies](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing) + for owner `cangelosilima`, repository `SyncSQL`. Use workflow filenames + `grammar-publish-nuget.yml` and `cli-publish-nuget.yml`. + Leave environment empty; the workflows do not use GitHub environments. + Scope each policy to its package ID and permit new packages as well as new + versions for the first publication. The account must own existing package IDs. +3. Allow these workflows to create release tags and GitHub releases. The publish + job requests `contents: write` and `id-token: write`. + +No long-lived NuGet API key is stored. The publish job obtains a temporary key +with GitHub OIDC after the package job succeeds. + +## Release and recovery + +Merge the reviewed changes into `main`; do not create tags manually. Only +`push` events on `refs/heads/main` may publish. Tag pushes, pull requests, +other branches, and manual workflow runs cannot publish or create releases. +Protect `main` with the repository's required reviews and checks to ensure +its push events come from approved merges. + +The CLI workflow watches CLI, grammar, SDK, and publishing-automation changes. +The grammar workflow watches grammar sources, its project, relevant Oracle +lineage/Core sources and tests, shared build settings, SDK, and its publishing +automation. A CLI-only change does not publish a new grammar package. + +For each selected package, the workflow: + +1. Runs tests, builds the package, and uploads a workflow artifact. +2. For the CLI, installs the packed tool and runs `syncsql --help`. +3. Reserves the generated tag at the exact tested main commit. +4. Publishes that package to NuGet. +5. Creates a draft GitHub release with generated notes, attaches the package, + then publishes the release. + +Rerun a failed workflow to recover. It reuses its version and tag, skips an +already-published NuGet version, and finishes an incomplete GitHub release. +An existing tag pointing to another commit is rejected and never moved. +A failure before NuGet publication can leave a reserved tag; a failure during +asset upload can leave a draft release. Neither triggers another publishing run. + +Pull requests and manual runs only validate, with versions suffixed +`-ci.RUN_ATTEMPT`. Use **Run workflow** for a dry run; there is no manual publish +option. GitHub releases for the two packages do not replace each other as the +repository's latest release. + +## Consume + +```bash +dotnet add package SyncSql.Grammar.PlSql +dotnet tool install --global SyncSql.Cli +``` + +The CLI requires the .NET 10 runtime. The grammar can be used independently by +.NET Standard 2.0 compatible applications. diff --git a/grammar/README.md b/grammar/README.md index 6a54c6e..e388e58 100644 --- a/grammar/README.md +++ b/grammar/README.md @@ -2,6 +2,19 @@ The complete Oracle SQL and PL/SQL parser used by `SyncSql.Lineage.Oracle`. +## NuGet package + +Install the standalone .NET Standard 2.0 parser with: + +```bash +dotnet add package SyncSql.Grammar.PlSql +``` + +The package includes the ANTLR runtime dependency and the upstream license and +notice. Merges into `main` release it independently of the CLI, automatically +creating `grammar-v*` tags and GitHub releases; see +[NuGet publishing](../docs/nuget-publishing.md) for setup and release instructions. + ## Java-free build design The human-readable grammar remains the Apache-2.0 `sql/plsql` grammar from