diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 00000000..fe7e2e7a --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,54 @@ +name: CI/CD + +on: + push: + branches: + - "**" + paths: + - .github/workflows/ci-cd.yml + - src/** + - scripts/** + - test/** + - Directory.Build.props + tags: + - "**" + workflow_dispatch: + +permissions: + contents: write # Needed to create a GitHub Release + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Build + - run: dotnet build --configuration Release + # Test + - run: dotnet test --configuration Release --no-build --collect:"XPlat Code Coverage" + env: + AWS_REGION: eu-west-1 + AWS_USER_WITH_PERMISSIONS_ACCESS_KEY_ID: ${{ secrets.AWS_USER_WITH_PERMISSIONS_ACCESS_KEY_ID }} + AWS_USER_WITH_PERMISSIONS_SECRET_ACCESS_KEY: ${{ secrets.AWS_USER_WITH_PERMISSIONS_SECRET_ACCESS_KEY }} + AWS_USER_WITHOUT_PERMISSIONS_ACCESS_KEY_ID: ${{ secrets.AWS_USER_WITHOUT_PERMISSIONS_ACCESS_KEY_ID }} + AWS_USER_WITHOUT_PERMISSIONS_SECRET_ACCESS_KEY: ${{ secrets.AWS_USER_WITHOUT_PERMISSIONS_SECRET_ACCESS_KEY }} + AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }} + AWS_API_GATEWAY_URL: ${{ secrets.AWS_API_GATEWAY_URL }} + AWS_S3_BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME }} + AWS_S3_BUCKET_URL: ${{ secrets.AWS_S3_BUCKET_URL }} + - uses: codecov/codecov-action@v3 + # Pack + - run: dotnet pack --configuration Release --no-build + - run: | + mkdir dist + mv src/bin/Release/*.nupkg ./dist + mv src/bin/Release/*.snupkg ./dist + # Release + - run: | + pushd ./scripts + npm ci + npm run release + popd + if: startsWith(github.ref, 'refs/tags/') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/infrastructure.yml b/.github/workflows/infrastructure.yml index 38d4efe6..f68e75c7 100644 --- a/.github/workflows/infrastructure.yml +++ b/.github/workflows/infrastructure.yml @@ -22,7 +22,6 @@ jobs: with: node-version: lts/* check-latest: true - - run: yarn --version - - run: yarn install --immutable --immutable-cache - - run: yarn build - - run: yarn lint + - run: npm ci + - run: npm run build + - run: npm run lint diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index ea6d65ac..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,55 +0,0 @@ -image: Visual Studio 2022 - -environment: - NODEJS_VERSION: "" # Resolves to Latest - AWS_REGION: eu-west-1 - AWS_USER_WITH_PERMISSIONS_ACCESS_KEY_ID: - secure: vFw/uLJUJt2p+fX3UwGUZLCZRQdc9qHBBXiD/opMGGA= - AWS_USER_WITH_PERMISSIONS_SECRET_ACCESS_KEY: - secure: GZ8x6ZSHrDRfIV6fvDrztXFF5fJfILXF+86hmejvODlWAYeFTiyyvP3VXwjnDp68 - AWS_USER_WITHOUT_PERMISSIONS_ACCESS_KEY_ID: - secure: YuI9fk7pbFyZK2K8a6OEztWih2nN1d1SuSTzJ95nMvY= - AWS_USER_WITHOUT_PERMISSIONS_SECRET_ACCESS_KEY: - secure: 75ZKko094t9BOeMupxCuhxnUfoHvZvszESQuKXWU61HO9WKkkozFcDh8oj0FwUbJ - AWS_ROLE_ARN: - secure: CVjaIRrwBKMLa+ZIqgA4E+IiYq5h16hz1UEvPGs0ZMizXZUYbb1km3rvJ8SIlxE1EXLeeD+oxKHszpaalDarUg== - AWS_API_GATEWAY_URL: - secure: Z+vOna9KGyaWi5k/U5ab36OfevgxyzvsKQLHEreflf7IfqAEETxl0mMVQRkPPzxgPY5n/uZi49JVXuezT9ofrA== - AWS_S3_BUCKET_NAME: - secure: 16d1j1uMCzq81pv5W2QQsGBUtu1Hou48y+Mfcfk8jGbBPVHRToKBjVZZussk3yZ9 - AWS_S3_BUCKET_URL: - secure: lvefZ1hszL1/6CpAqHdHniRnLQ1zuNeTJnGSkvVZV/r9KJdh6t+GjVPlS8iAgraKG2WicZ1OOi2D38nenkkWW2Eh8V0vxx9XIaUO0oQguGk= - CODECOV_TOKEN: - secure: qzGgvFnseXRU4WdzFJUHKSLk84TcURa3dF8ynmrtqbvy/2tJXn9giNHUJBVNcFzy - -install: - - ps: Install-Product node $env:NODEJS_VERSION - -build_script: - - ps: ./build/build.ps1 - -test: off - -artifacts: - - path: artifacts/*.*nupkg - name: NuGet - type: NuGetPackage - -deploy: - - provider: GitHub - tag: ${APPVEYOR_REPO_TAG_NAME} - release: Release ${APPVEYOR_REPO_TAG_NAME} - description: TODO - auth_token: - secure: ZglZz47GHglp4+AsrdTPgicFyCZK7eg2LkJ77egmhcFnetbGtkHQef5aKhdUTb9S - artifact: NuGet - draft: true - on: - APPVEYOR_REPO_TAG: true - - provider: NuGet - api_key: - secure: C+9lUIZI3DmgSVqLAFvF1Paqaj5fktjUw0jwucnh6k/Qf41cyslLOpZ29AQTWsVr - symbol_server: https://www.nuget.org - skip_symbols: false - on: - APPVEYOR_REPO_TAG: true diff --git a/build/build.ps1 b/build/build.ps1 deleted file mode 100644 index 91157f55..00000000 --- a/build/build.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# COMMON FUNCTIONS -# ------------------------------------------------------------------------------------------------- -function Print { - param ( - [string]$Category, - [string]$Message - ) - - if ($Category) { - Write-Host "[$Category] $Message" -ForegroundColor Green - } else { - Write-Host "$Message" -ForegroundColor Green - } -} - -function AssertLastExitCode { - if ($LASTEXITCODE -ne 0) { - exit 1 - } -} - -# ------------------------------------------------------------------------------------------------- -# LOGO -# ------------------------------------------------------------------------------------------------- -$logo = (Invoke-WebRequest "https://raw.githubusercontent.com/FantasticFiasco/logo/master/logo.raw").toString(); -Print -Message $logo - -# ------------------------------------------------------------------------------------------------- -# VARIABLES -# ------------------------------------------------------------------------------------------------- -$git_sha = "$env:APPVEYOR_REPO_COMMIT".TrimStart("0").substring(0, 7) -$is_tagged_build = If ("$env:APPVEYOR_REPO_TAG" -eq "true") { $true } Else { $false } -$is_pull_request = If ("$env:APPVEYOR_PULL_REQUEST_NUMBER" -eq "") { $false } Else { $true } -Print "info" "git sha: $git_sha" -Print "info" "is git tag: $is_tagged_build" -Print "info" "is pull request: $is_pull_request" - -# ------------------------------------------------------------------------------------------------- -# BUILD -# ------------------------------------------------------------------------------------------------- -Print "build" "build started" -Print "build" "dotnet cli v$(dotnet --version)" - -[xml]$build_props = Get-Content -Path .\Directory.Build.props -$version_prefix = $build_props.Project.PropertyGroup.VersionPrefix -Print "info" "build props version prefix: $version_prefix" -$version_suffix = $build_props.Project.PropertyGroup.VersionSuffix -Print "info" "build props version suffix: $version_suffix" - -if ($is_tagged_build) { - Print "build" "build" - dotnet build -c Release - AssertLastExitCode - - Print "build" "pack" - New-Item -ItemType Directory -Path .\artifacts - dotnet pack -c Release --no-build - AssertLastExitCode - Move-Item -Path .\src\bin\Release\*.nupkg -Destination .\artifacts - Move-Item -Path .\src\bin\Release\*.snupkg -Destination .\artifacts -} else { - # Use git tag if version suffix isn't specified - if ($version_suffix -eq "") { - $version_suffix = $git_sha - } - - Print "build" "build" - dotnet build -c Release --version-suffix=$version_suffix - AssertLastExitCode - - Print "build" "pack" - New-Item -ItemType Directory -Path .\artifacts - dotnet pack -c Release --version-suffix=$version_suffix --no-build - AssertLastExitCode - Move-Item -Path .\src\bin\Release\*.nupkg -Destination .\artifacts - Move-Item -Path .\src\bin\Release\*.snupkg -Destination .\artifacts -} - -# ------------------------------------------------------------------------------------------------- -# TEST -# ------------------------------------------------------------------------------------------------- -Print "test" "test started" - -if ($is_pull_request -eq $true) { - # Exclude integration tests if we run as part of a pull requests. Integration tests rely on - # secrets, which are omitted by AppVeyor on pull requests. - dotnet test -c Release --no-build --filter Category!=Integration - AssertLastExitCode -} else { - dotnet test -c Release --no-build --collect:"XPlat Code Coverage" - AssertLastExitCode - - Print "test" "download codecov uploader" - Invoke-WebRequest -Uri https://uploader.codecov.io/latest/codecov.exe -Outfile codecov.exe - - foreach ($test_result in Get-ChildItem .\test\TestResults\*\coverage.cobertura.xml) - { - $relative_test_result = $test_result | Resolve-Path -Relative - - # CodeCode uploader cant handle "\", thus we have to replace these with "/" - $relative_test_result = $relative_test_result -Replace "\\", "/" - - Print "test" "upload coverage report $relative_test_result" - - .\codecov.exe -f $relative_test_result - AssertLastExitCode - } -} - -# ------------------------------------------------------------------------------------------------- -# INFRASTRUCTURE -# ------------------------------------------------------------------------------------------------- -Print "infrastructure" "build started" -Print "infrastructure" "node $(node --version)" -cd ./infrastructure -npm ci -npm run build diff --git a/infrastructure/lib/api-gateway/handlers/request.ts b/infrastructure/lib/api-gateway/handlers/request.ts index 0d5bbee1..3e25ee2c 100644 --- a/infrastructure/lib/api-gateway/handlers/request.ts +++ b/infrastructure/lib/api-gateway/handlers/request.ts @@ -9,8 +9,8 @@ interface HttpResponse { interface ReceivedRequest { method: string path: string - queryStringParameters: { [name: string]: string[] | undefined; } | null - headers: { [name: string]: string[] | undefined; } | null + queryStringParameters: { [name: string]: string[] | undefined } | null + headers: { [name: string]: string[] | undefined } | null body: string | null } diff --git a/infrastructure/lib/users/users-stack.ts b/infrastructure/lib/users/users-stack.ts index 49e46648..f5ac9671 100644 --- a/infrastructure/lib/users/users-stack.ts +++ b/infrastructure/lib/users/users-stack.ts @@ -75,7 +75,7 @@ export class UsersStack extends Stack { new PolicyStatement({ actions: ['execute-api:Invoke', 'execute-api:ManageConnections'], resources: ['arn:aws:execute-api:*:*:*'], - }) + }), ) // Create outputs diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 00000000..07e6e472 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/scripts/github-actions.js b/scripts/github-actions.js new file mode 100644 index 00000000..5a4871e4 --- /dev/null +++ b/scripts/github-actions.js @@ -0,0 +1,12 @@ +// @ts-check + +// GitHub token +export const GITHUB_TOKEN = process.env.GITHUB_TOKEN ?? ''; + +// git tag +const prefix = 'refs/tags/'; +const ref = process.env.GITHUB_REF ?? ''; +export const GIT_TAG = ref.startsWith(prefix) ? ref.substring(prefix.length) : ''; + +// Repo slug, e.g. "owner_name/repo_name" +export const REPO = process.env.GITHUB_REPOSITORY ?? ''; diff --git a/scripts/github.js b/scripts/github.js new file mode 100644 index 00000000..0924c670 --- /dev/null +++ b/scripts/github.js @@ -0,0 +1,57 @@ +// @ts-check + +import { Octokit } from '@octokit/rest'; +import { readFileSync } from 'fs'; +import { basename } from 'path'; +import { info } from './log.js'; + +/** + * @param {string} githubToken + * @param {string} owner + * @param {string} repo + * @param {string} tagName + * @param {string} version + */ +export const createRelease = async (githubToken, owner, repo, tagName, version) => { + info(`github: create release from tag ${tagName}`); + + const octokit = new Octokit({ + auth: githubToken, + }); + + const release = await octokit.repos.createRelease({ + owner, + repo, + tag_name: tagName, + name: `Release ${version}`, + body: 'TODO', + draft: true, + }); + + return { + releaseId: release.data.id, + }; +}; + +/** + * @param {string} githubToken + * @param {string} owner + * @param {string} repo + * @param {number} releaseId + * @param {string} assetFileName + */ +export const uploadAsset = async (githubToken, owner, repo, releaseId, assetFileName) => { + info(`github: upload asset ${assetFileName}`); + + const octokit = new Octokit({ + auth: githubToken, + }); + + await octokit.repos.uploadReleaseAsset({ + owner, + repo, + release_id: releaseId, + name: basename(assetFileName), + data: readFileSync(assetFileName).toString(), + }); +}; diff --git a/scripts/log.js b/scripts/log.js new file mode 100644 index 00000000..75129135 --- /dev/null +++ b/scripts/log.js @@ -0,0 +1,34 @@ +// @ts-check + +export const RED = '[31m'; +export const YELLOW = '[33;1m'; + +/** + * @param {string} message + */ +export const info = (message) => { + console.log(message); +}; + +/** + * @param {string} message + */ +export const error = (message) => { + log(RED, message); +}; + +/** + * @param {string} message + */ +export const fatal = (message) => { + log(RED, message); + process.exitCode = 1; +}; + +/** + * @param {string} color + * @param {string} message + */ +export const log = (color, message) => { + console.log('\x1b%s%s\x1b[0m', color, message); +}; diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 00000000..416715f6 --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,214 @@ +{ + "name": "scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scripts", + "version": "1.0.0", + "devDependencies": { + "@octokit/rest": "20.0.2" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.1.tgz", + "integrity": "sha512-lyeeeZyESFo+ffI801SaBKmCfsvarO+dgV8/0gD8u1d87clbEdWsP5yC+dSj3zLhb2eIf5SJrn6vDz9AheETHw==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.1.tgz", + "integrity": "sha512-hRlOKAovtINHQPYHZlfyFwaM8OyetxeoC81lAkBy34uLb8exrZB50SQdeW3EROqiY9G9yxQTpp5OHTV54QD+vA==", + "dev": true, + "dependencies": { + "@octokit/types": "^12.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "dev": true, + "dependencies": { + "@octokit/request": "^8.0.1", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.0.0.tgz", + "integrity": "sha512-PclQ6JGMTE9iUStpzMkwLCISFn/wDeRjkZFIKALpvJQNBGwDoYYi2fFvuHwssoQ1rXI5mfh6jgTgWuddeUzfWw==", + "dev": true + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.0.0.tgz", + "integrity": "sha512-oIJzCpttmBTlEhBmRvb+b9rlnGpmFgDtZ0bB6nq39qIod6A5DP+7RkVLMOixIgRCYSHDTeayWqmiJ2SZ6xgfdw==", + "dev": true, + "dependencies": { + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.0.tgz", + "integrity": "sha512-2uJI1COtYCq8Z4yNSnM231TgH50bRkheQ9+aH8TnZanB6QilOnx8RMD2qsnamSOXtDj0ilxvevf5fGsBhBBzKA==", + "dev": true, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.0.1.tgz", + "integrity": "sha512-fgS6HPkPvJiz8CCliewLyym9qAx0RZ/LKh3sATaPfM41y/O2wQ4Z9MrdYeGPVh04wYmHFmWiGlKPC7jWVtZXQA==", + "dev": true, + "dependencies": { + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/request": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.4.tgz", + "integrity": "sha512-M0aaFfpGPEKrg7XoA/gwgRvc9MSXHRO2Ioki1qrPDbl1e9YhjIwVoHE7HIKmv/m3idzldj//xBujcFNqGX6ENA==", + "dev": true, + "dependencies": { + "@octokit/endpoint": "^9.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", + "dev": true, + "dependencies": { + "@octokit/types": "^12.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz", + "integrity": "sha512-Ux8NDgEraQ/DMAU1PlAohyfBBXDwhnX2j33Z1nJNziqAfHi70PuxkFYIcIt8aIAxtRE7KVuKp8lSR8pA0J5iOQ==", + "dev": true, + "dependencies": { + "@octokit/core": "^5.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.0.0.tgz", + "integrity": "sha512-EzD434aHTFifGudYAygnFlS1Tl6KhbTynEWELQXIbTY8Msvb5nEqTZIm7sbPEt4mQYLZwu3zPKVdeIrw0g7ovg==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^19.0.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 00000000..76550538 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,13 @@ +{ + "name": "scripts", + "version": "1.0.0", + "description": "", + "private": true, + "type": "module", + "scripts": { + "release": "node ./publish-on-tag.js" + }, + "devDependencies": { + "@octokit/rest": "20.0.2" + } +} diff --git a/scripts/publish-on-tag.js b/scripts/publish-on-tag.js new file mode 100644 index 00000000..afc41a14 --- /dev/null +++ b/scripts/publish-on-tag.js @@ -0,0 +1,65 @@ +// @ts-check + +import { readdir } from 'fs/promises'; +import { join } from 'path'; +import { GITHUB_TOKEN, GIT_TAG, REPO } from './github-actions.js'; +import { createRelease, uploadAsset } from './github.js'; +import { fatal } from './log.js'; + +/** + * A tagged commit in this repo is created using the following format: + * + * v + * + * where is the semantic version (SemVer) of the package. + * + * The following tag would satisfy the format: + * + * v1.2.3-alpha + */ +const assertGitTag = () => { + if (!GIT_TAG) { + fatal('Aborting a deployment to GitHub Releases because this is not a tagged commit'); + return null; + } + + // Remove the 'v' prefix and assert that it's a valid SemVer version + const tag = GIT_TAG.replace(/^v/, ''); + const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/gm; + if (!semverRegex.test(tag)) { + fatal(`Aborting deployment to GitHub Releases because "${tag}" does not conform to SemVer`); + } + + return GIT_TAG; +}; + +const parseRepo = () => { + const [owner, repo] = REPO.split('/'); + + return { + owner, + repo, + }; +}; + +const main = async () => { + const version = assertGitTag(); + if (!version) { + return; + } + + const { owner, repo } = parseRepo(); + + // Create GitHub release + const { releaseId } = await createRelease(GITHUB_TOKEN, owner, repo, GIT_TAG, version); + + const distDir = join('..', 'dist'); + for (const asset of await readdir(distDir)) { + const fileName = join(distDir, asset); + await uploadAsset(GITHUB_TOKEN, owner, repo, releaseId, fileName); + } +}; + +main().catch((err) => { + fatal(err); +});