diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 6587f175ef..010757e3b8 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -1,4 +1,4 @@ -name: Deploy Preview +name: Deploy Website (Preview) on: workflow_run: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3dceea09a1..62920ce2ac 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,16 +1,18 @@ -name: Deploy +name: Deploy Website on: push: branches: - main + tags: + - 'v*.*.*' concurrency: group: "deploy" - cancel-in-progress: true jobs: - deploy: + deploy-branch: + if: startsWith(github.ref, 'refs/heads/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -35,3 +37,31 @@ jobs: git add . git commit -m "Website updates" git push origin main + deploy-tag: + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Install dependencies + run: npm ci + - name: Assert Latest Release + run: node tasks/newest-tag.js --tag ${GITHUB_REF_NAME} + - name: Build Website + run: ./tasks/build-website.sh -l ${GITHUB_REF_NAME} -v ${GITHUB_REF_NAME} + - name: Check out openlayers.github.io + uses: actions/checkout@v3 + with: + repository: openlayers/openlayers.github.io + ssh-key: ${{ secrets.OPENLAYERS_GITHUB_IO_KEY }} + path: openlayers.github.io + - run: | + cp -r build/site/* openlayers.github.io/dist/ + cd openlayers.github.io + git config user.name "$(git --no-pager log --format=format:'%an' -n 1)" + git config user.email "$(git --no-pager log --format=format:'%ae' -n 1)" + git add . + git commit -m "Website updates" + git push origin main diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f679b4556b..f372dd13a3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,21 +1,24 @@ -name: Publish +name: Publish Package on: push: branches: - main + tags: + - 'v*.*.*' permissions: contents: read jobs: - publish-npm: + publish-branch: + if: startsWith(github.ref, 'refs/heads/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: '16' + node-version: '18' registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm ci @@ -28,3 +31,23 @@ jobs: npm publish --tag dev env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + publish-tag: + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '18' + registry-url: 'https://registry.npmjs.org' + - name: Install dependencies + run: npm ci + - name: Assert Latest Release + run: node tasks/newest-tag.js --tag ${GITHUB_REF_NAME} + - name: Publish + run: | + npm run build-package + cd build/ol + npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..06da363e7b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: Create Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Install dependencies + run: npm ci + - name: Build Release Assets + run: ./tasks/build-website.sh -l ${GITHUB_REF_NAME} -v ${GITHUB_REF_NAME} + - name: Create Release + run: node tasks/create-release.js --token ${{secrets.GITHUB_TOKEN}} --tag ${GITHUB_REF_NAME} --legacy build/${GITHUB_REF_NAME}-legacy.zip --site build/${GITHUB_REF_NAME}-site.zip diff --git a/tasks/build-website.sh b/tasks/build-website.sh index a9704127ab..3335d9d63d 100755 --- a/tasks/build-website.sh +++ b/tasks/build-website.sh @@ -78,4 +78,13 @@ mv build/legacy ${build}/en/${version}/ if [[ "${latest}" == "${version}" ]] ; then echo "Copying to en/latest" cp -r ${build}/en/${version} ${build}/en/latest + + echo "Building release artifacts" + pushd ${build} + zip -r ${OLDPWD}/build/${version}-site.zip . -x "en/${version}/*" + popd + + pushd ${build}/en/${version}/legacy + zip -r ${OLDPWD}/build/${version}-legacy.zip . + popd fi diff --git a/tasks/create-release.js b/tasks/create-release.js new file mode 100644 index 0000000000..e35c7a4c70 --- /dev/null +++ b/tasks/create-release.js @@ -0,0 +1,107 @@ +import esMain from 'es-main'; +import yargs from 'yargs'; +import {Octokit} from '@octokit/rest'; +import {basename} from 'node:path'; +import {hideBin} from 'yargs/helpers'; +import {readFile, stat} from 'node:fs/promises'; + +/** + * @typedef {Object} Options + * @property {string} token The bearer token. + * @property {string} tag The tag. + * @property {boolean} draft Create a draft release. + * @property {boolean} notes Generate release notes. + * @property {string} site Path to zip archive with site contents. + * @property {string} legacy Path to zip archive with legacy build. + */ + +const owner = 'openlayers'; +const repo = 'openlayers'; + +/** + * Create a release. + * @param {Options} options The release options. + */ +async function createRelease(options) { + const client = new Octokit({ + auth: options.token, + }); + + const response = await client.rest.repos.createRelease({ + owner, + repo, + tag_name: options.tag, + generate_release_notes: options.notes, + draft: options.draft, + }); + + await uploadAsset( + client, + response.data, + options.site, + 'Examples and docs (zip)' + ); + + await uploadAsset( + client, + response.data, + options.legacy, + 'Legacy build (zip)' + ); +} + +async function uploadAsset(client, release, assetPath, label) { + const name = basename(assetPath); + const stats = await stat(assetPath); + const data = await readFile(assetPath); + + await client.rest.repos.uploadReleaseAsset({ + url: release.upload_url, + name, + label, + headers: { + 'content-type': 'application/zip', + 'content-length': stats.size, + }, + data, + }); +} + +if (esMain(import.meta)) { + const options = yargs(hideBin(process.argv)) + .option('token', { + describe: 'The token for auth', + type: 'string', + }) + .demandOption('token') + .option('tag', { + describe: 'The release tag (e.g. v7.0.0)', + type: 'string', + }) + .demandOption('tag') + .option('legacy-zip', { + describe: 'Path to the archive with the legacy build', + type: 'string', + }) + .demandOption('legacy') + .option('site-zip', { + describe: 'Path to the archive with the site contents', + type: 'string', + }) + .demandOption('site') + .option('draft', { + describe: 'Create a draft release', + type: 'boolean', + default: true, + }) + .option('notes', { + describe: 'Generate release notes', + type: 'boolean', + default: true, + }) + .parse(); + + createRelease(options).catch((err) => { + process.stderr.write(`${err.stack}\n`, () => process.exit(1)); + }); +} diff --git a/tasks/get-latest-release.js b/tasks/get-latest-release.js index dc971fc01a..4cdbbfa641 100644 --- a/tasks/get-latest-release.js +++ b/tasks/get-latest-release.js @@ -1,7 +1,8 @@ +import esMain from 'es-main'; import semver from 'semver'; import {Octokit} from '@octokit/rest'; -async function main() { +export async function getLatestRelease() { const client = new Octokit(); let latest = '0.0.0'; @@ -21,7 +22,15 @@ async function main() { } ); - process.stdout.write(`v${latest}\n`); + return latest; } -main(); +if (esMain(import.meta)) { + getLatestRelease() + .then((latest) => { + process.stdout.write(`v${latest}\n`, () => process.exit(0)); + }) + .catch((err) => { + process.stderr.write(`${err.message}\n`, () => process.exit(1)); + }); +} diff --git a/tasks/newest-tag.js b/tasks/newest-tag.js new file mode 100644 index 0000000000..6887343df3 --- /dev/null +++ b/tasks/newest-tag.js @@ -0,0 +1,52 @@ +import esMain from 'es-main'; +import semver from 'semver'; +import yargs from 'yargs'; +import {getLatestRelease} from './get-latest-release.js'; +import {hideBin} from 'yargs/helpers'; + +/** + * @typedef {Object} Options + * @property {string} tag The tag. + */ + +/** + * Check if a tag is ahead of the latest release. + * @param {Options} options The options. + * @return {boolean} The provided tag is ahead of or equal to the latest release. + */ +async function main(options) { + const version = semver.valid(options.tag); + if (!version) { + return false; + } + + const parsed = semver.parse(version); + if (parsed.prerelease.length) { + return false; + } + + const latest = await getLatestRelease(); + return semver.gte(version, latest); +} + +if (esMain(import.meta)) { + const options = yargs(hideBin(process.argv)) + .option('tag', { + describe: 'The tag to test (e.g. v1.2.3)', + type: 'string', + }) + .demandOption('tag') + .parse(); + + main(options) + .then((newest) => { + if (newest) { + process.stdout.write('true\n', () => process.exit(0)); + } else { + process.stderr.write('false\n', () => process.exit(1)); + } + }) + .catch((err) => { + process.stderr.write(`${err.stack}\n`, () => process.exit(1)); + }); +}