diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 53c85c4..0000000 --- a/.drone.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -kind: pipeline -type: docker -name: release - -# Cuts the signed PGXN distribution and publishes it as a Gitea release. -# Triggered by version tags (v..). -# -# Repo must be Trusted in Drone. Required secrets: -# pgmorbac_release_key Ed25519 private key PEM (content, not a path) -# gitea_token Gitea API token with write access to crudy/pgmorbac - -trigger: - event: - - tag - ref: - - refs/tags/v* - -steps: - - name: build-and-release - image: node:24-alpine - environment: - PGMORBAC_RELEASE_KEY_PEM: - from_secret: pgmorbac_release_key - GITEA_TOKEN: - from_secret: gitea_token - GITEA_URL: https://git.villains.fr - GITEA_REPO: crudy/pgmorbac - commands: - - apk add --no-cache zip bash - - npm install --no-audit --no-fund - - umask 077 && printf '%s\n' "$PGMORBAC_RELEASE_KEY_PEM" > /tmp/release-key.pem - - PGMORBAC_RELEASE_KEY=/tmp/release-key.pem node scripts/build-dist.mjs - - node scripts/gitea-release.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..85b7e12 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +# Cuts the signed PGXN distribution and publishes it as a GitHub release on +# version tags (v..). Secret: PGMORBAC_RELEASE_KEY, the +# Ed25519 private key PEM (content, not a path). +name: release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: npm install --no-audit --no-fund + - name: Build and sign + env: + PGMORBAC_RELEASE_KEY_PEM: ${{ secrets.PGMORBAC_RELEASE_KEY }} + run: | + umask 077 + printf '%s\n' "$PGMORBAC_RELEASE_KEY_PEM" > "$RUNNER_TEMP/release-key.pem" + PGMORBAC_RELEASE_KEY="$RUNNER_TEMP/release-key.pem" node scripts/build-dist.mjs + - name: Publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node scripts/github-release.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index d14b108..b64131f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,4 +55,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `rls_check(activity, view, row_org_id[, row_user_id])` for row-scoped tables where a NULL `row_org_id` means the record is unattributed -[1.0.0]: https://git.villains.fr/crudy/pgmorbac/releases/tag/v1.0.0 +[1.0.0]: https://github.com/crudylabs/pgmorbac/releases/tag/v1.0.0 diff --git a/META.json b/META.json index 30b70e8..9fa4910 100644 --- a/META.json +++ b/META.json @@ -22,7 +22,7 @@ } }, "resources": { - "homepage": "https://pgmorbac.villains.fr", + "homepage": "https://pgmorbac.crudy.fr", "bugtracker": { "web": "https://github.com/crudylabs/pgmorbac/issues" }, diff --git a/README.md b/README.md index 542f5b4..1e4de56 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ git push --tags ## Support -- Read the [documentation](https://pgmorbac.villains.fr) +- Read the [documentation](https://pgmorbac.crudy.fr) - Report bugs via [GitHub Issues](https://github.com/crudylabs/pgmorbac/issues) - Security issues: see [SECURITY.md](SECURITY.md) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 4e0e48d..e3fa479 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -67,12 +67,13 @@ git tag vX.Y.Z git push origin master --tags ``` -5. Signed SQL distribution, attached to the Gitea release (Drone does this on the - tag when the repository is activated there): +5. Signed SQL distribution, attached to the GitHub release. The `release` + workflow does this on the tag, signing with the `PGMORBAC_RELEASE_KEY` + repository secret. By hand: ```bash -PGMORBAC_RELEASE_KEY= GITEA_URL=https://git.villains.fr \ - GITEA_REPO=crudy/pgmorbac GITEA_TOKEN= npm run release +PGMORBAC_RELEASE_KEY= GITHUB_REPOSITORY=crudylabs/pgmorbac \ + GITHUB_TOKEN= npm run release ``` 6. The npm package `@crudy/pgmorbac` is released from the `pgmorbac-node` diff --git a/package.json b/package.json index 555f709..e0353e1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "description": "Release tooling for the pgmorbac PostgreSQL extension (build + sign + publish the PGXN distribution).", "scripts": { "dist": "node scripts/build-dist.mjs", - "release": "node scripts/build-dist.mjs && node scripts/gitea-release.mjs" + "release": "node scripts/build-dist.mjs && node scripts/github-release.mjs" }, "devDependencies": { "jose": "^5" diff --git a/scripts/build-dist.mjs b/scripts/build-dist.mjs index 4af75db..6637028 100644 --- a/scripts/build-dist.mjs +++ b/scripts/build-dist.mjs @@ -66,7 +66,7 @@ include $(PGXS) writeFileSync(join(stageDir, 'README-INSTALL.md'), `# Installing pgmorbac ${version} -Full documentation: https://pgmorbac.villains.fr +Full documentation: ${meta.resources.homepage} ## As a PostgreSQL extension (recommended) diff --git a/scripts/gitea-release.mjs b/scripts/gitea-release.mjs deleted file mode 100644 index 1a06c9d..0000000 --- a/scripts/gitea-release.mjs +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env node -// Creates (or reuses) release v on Gitea and attaches the dist assets. -// Run after build-dist.mjs. Requires GITEA_URL, GITEA_REPO, GITEA_TOKEN. - -import { readFileSync, existsSync } from 'fs'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; - -const root = join(dirname(fileURLToPath(import.meta.url)), '..'); -const version = readFileSync(join(root, 'pgmorbac.control'), 'utf8').match(/default_version\s*=\s*'([^']+)'/)?.[1]; -if (!version) { - console.error('[gitea-release] could not read default_version from pgmorbac.control'); - process.exit(1); -} - -const base = process.env.GITEA_URL; -const repo = process.env.GITEA_REPO; -const token = process.env.GITEA_TOKEN; -if (!base || !repo || !token) { - console.error('[gitea-release] GITEA_URL, GITEA_REPO and GITEA_TOKEN are required'); - process.exit(1); -} - -const distDir = join(root, 'dist'); -const files = [ - `pgmorbac-${version}.zip`, - `pgmorbac-${version}.zip.sha256`, - `pgmorbac-${version}.manifest.jwt`, - `pgmorbac-${version}.pub.pem`, -]; -for (const f of files) { - if (!existsSync(join(distDir, f))) { - console.error(`[gitea-release] Missing artifact ${f} - run build-dist.mjs first`); - process.exit(1); - } -} - -const api = (path) => `${base}/api/v1/repos/${repo}${path}`; -const headers = { Authorization: `token ${token}` }; - -async function ensureRelease() { - const create = await fetch(api('/releases'), { - method: 'POST', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tag_name: `v${version}`, - name: `pgmorbac ${version}`, - body: `PGXN-compatible SQL distribution. Verify with sha256sum -c and the signed manifest (public key attached; see https://pgmorbac.villains.fr/download).`, - draft: false, - prerelease: false, - }), - }); - if (create.ok) return (await create.json()).id; - if (create.status !== 409) { - console.error(`[gitea-release] Release creation failed: ${create.status} ${await create.text()}`); - process.exit(1); - } - const existing = await fetch(api(`/releases/tags/v${version}`), { headers }); - if (!existing.ok) { - console.error(`[gitea-release] Release exists but lookup failed: ${existing.status}`); - process.exit(1); - } - return (await existing.json()).id; -} - -const releaseId = await ensureRelease(); -for (const f of files) { - const form = new FormData(); - form.set('attachment', new Blob([readFileSync(join(distDir, f))]), f); - const res = await fetch(api(`/releases/${releaseId}/assets?name=${encodeURIComponent(f)}`), { - method: 'POST', headers, body: form, - }); - if (!res.ok) { - const text = await res.text(); - if (res.status === 409 || text.includes('already exist')) { - console.log(`[gitea-release] ${f} already attached - skipping`); - continue; - } - console.error(`[gitea-release] Upload of ${f} failed: ${res.status} ${text}`); - process.exit(1); - } - console.log(`[gitea-release] Attached ${f}`); -} -console.log(`[gitea-release] Release v${version} ready on ${repo}`); diff --git a/scripts/github-release.mjs b/scripts/github-release.mjs new file mode 100644 index 0000000..984c542 --- /dev/null +++ b/scripts/github-release.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// Creates (or reuses) release v on GitHub and attaches the dist assets. +// Run after build-dist.mjs. Requires GITHUB_REPOSITORY (owner/name) and GITHUB_TOKEN. + +import { readFileSync, existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const version = readFileSync(join(root, 'pgmorbac.control'), 'utf8').match(/default_version\s*=\s*'([^']+)'/)?.[1]; +if (!version) { + console.error('[github-release] could not read default_version from pgmorbac.control'); + process.exit(1); +} +const homepage = JSON.parse(readFileSync(join(root, 'META.json'), 'utf8')).resources.homepage; + +const repo = process.env.GITHUB_REPOSITORY; +const token = process.env.GITHUB_TOKEN; +if (!repo || !token) { + console.error('[github-release] GITHUB_REPOSITORY and GITHUB_TOKEN are required'); + process.exit(1); +} + +const distDir = join(root, 'dist'); +const files = [ + `pgmorbac-${version}.zip`, + `pgmorbac-${version}.zip.sha256`, + `pgmorbac-${version}.manifest.jwt`, + `pgmorbac-${version}.pub.pem`, +]; +for (const f of files) { + if (!existsSync(join(distDir, f))) { + console.error(`[github-release] Missing artifact ${f} - run build-dist.mjs first`); + process.exit(1); + } +} + +const headers = { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' }; +const api = (path) => `https://api.github.com/repos/${repo}${path}`; + +async function ensureRelease() { + const existing = await fetch(api(`/releases/tags/v${version}`), { headers }); + if (existing.ok) return existing.json(); + if (existing.status !== 404) { + console.error(`[github-release] Release lookup failed: ${existing.status}`); + process.exit(1); + } + const create = await fetch(api('/releases'), { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tag_name: `v${version}`, + name: `pgmorbac ${version}`, + body: `PGXN-compatible SQL distribution. Verify with sha256sum -c and the signed manifest (public key attached; see ${homepage}/download).`, + draft: false, + prerelease: false, + }), + }); + if (!create.ok) { + console.error(`[github-release] Release creation failed: ${create.status} ${await create.text()}`); + process.exit(1); + } + return create.json(); +} + +const release = await ensureRelease(); +const attached = new Set((release.assets ?? []).map((a) => a.name)); +for (const f of files) { + if (attached.has(f)) { + console.log(`[github-release] ${f} already attached - skipping`); + continue; + } + const res = await fetch(`https://uploads.github.com/repos/${repo}/releases/${release.id}/assets?name=${encodeURIComponent(f)}`, { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/octet-stream' }, + body: readFileSync(join(distDir, f)), + }); + if (!res.ok) { + console.error(`[github-release] Upload of ${f} failed: ${res.status} ${await res.text()}`); + process.exit(1); + } + console.log(`[github-release] Attached ${f}`); +} +console.log(`[github-release] Release v${version} ready on ${repo}`);