diff --git a/.github/workflows/canary-release.yml b/.github/workflows/canary-release.yml new file mode 100644 index 00000000..2ab6e7d2 --- /dev/null +++ b/.github/workflows/canary-release.yml @@ -0,0 +1,54 @@ +name: Canary Release +on: + pull_request: +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + publish-canary: + if: startsWith(github.head_ref, 'topic-') + runs-on: ubuntu-latest + steps: + - name: Get branch name + shell: bash + run: echo "name=$(echo $GITHUB_HEAD_REF | sed 's/refs\/heads\///' | tr '/' '-')" >> $GITHUB_OUTPUT + id: branch_name + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: 1 + ref: ${{ github.event.pull_request.head.sha }} + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Publish + id: publish + run: | + echo ".npmrc" >> .git/info/exclude + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc + mkdir ${{ runner.temp }}/${{ steps.branch_name.outputs.name }} + yarn lerna publish prerelease --canary --no-private --no-verify-access --yes --exact --dist-tag=${{ steps.branch_name.outputs.name }} --preid=${{ steps.branch_name.outputs.name }} --summary-file=${{ runner.temp }}/${{ steps.branch_name.outputs.name }} + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const fs = require('fs'); + const summary = JSON.parse(fs.readFileSync('${{ runner.temp }}/${{ steps.branch_name.outputs.name }}/lerna-publish-summary.json', 'utf8')); + const packages = summary.map(pkg => `**${pkg.packageName}@${pkg.version}**\n\`\`\`bash\nyarn add ${pkg.packageName}@${{ steps.branch_name.outputs.name }}\n\`\`\``).join('\n'); + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `🎉 Canary Release!\n\n${packages}` + }) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 00000000..9d7da801 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,116 @@ +name: Prerelease +on: + push: + branches: + - 'main' +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + prerelease: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: 1 + + - name: Extract version from lerna.json + run: echo "value=$(jq .version lerna.json -r)" >> $GITHUB_OUTPUT + id: current_version + + - name: Determine Prerelease Preid + shell: bash + run: echo "value=$([[ ${{ steps.current_version.outputs.value }} =~ beta ]] && echo "beta" || echo "alpha")" >> $GITHUB_OUTPUT + id: preid + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Check for changed packages + id: changes + run: | + error_filename="${{ runner.temp }}/changes_errors.log"; + changes=$(echo $(yarn -s lerna changed --json --loglevel 2>>$error_filename) 2>>$error_filename); + cat $error_filename; + count=$([[ -n $changes ]] && echo $(echo $changes | jq '. | map(select(.private == false)) | length')) || echo 0 + changed=$([[ $changes > 0 ]] && echo "true" || echo "false") + echo "changes=$count" >> $GITHUB_OUTPUT + echo "changed=$changed" >> $GITHUB_OUTPUT + + echo "changes: $changes - changed: $changed" + + - name: Publish + id: publish + if: ${{ steps.changes.outputs.changed == 'true' }} + run: | + git config --global user.name 'github-actions' + git config --global user.email 'github-actions@users.noreply.github.com' + echo ".npmrc" >> .git/info/exclude + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc + yarn lerna version prerelease --no-private --conventional-commits --conventional-prerelease --preid=${{ steps.preid.outputs.value }} --yes + yarn lerna publish from-git --summary-file=${{ runner.temp }} --yes + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run release-pr.yaml workflow + if: ${{ steps.changes.outputs.changed == 'true' }} + run: | + tag=$(git describe --tags $(git rev-list --tags --max-count=1)) + echo "${{ runner.temp }}/lerna-publish-summary.json"; + if [[ -f "${{ runner.temp }}/lerna-publish-summary.json" ]]; then + gh workflow run release-pr.yml --ref $tag; + fi; + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get release commit + if: ${{ steps.changes.outputs.changed == 'true' }} + shell: bash + run: echo "sha=$(git rev-parse --verify HEAD)" >> $GITHUB_OUTPUT + id: release_commit + + - uses: actions/github-script@v6 + if: ${{ steps.changes.outputs.changed == 'true' }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs/promises"); + let summary = null; + + try { + summary = await fs.readFile( + "${{ runner.temp }}/lerna-publish-summary.json", + "utf8" + ); + } catch { + console.log("No new releases!"); + } + + if (!summary) return; + + summary = JSON.parse(summary); + + const packages = summary + .map( + (pkg) => + `**${pkg.packageName}@${pkg.version}**\n\`\`\`bash\nyarn add ${pkg.packageName}@${pkg.version}\n\`\`\`` + ) + .join("\n"); + + const preid = "${{ steps.preid.outputs.value }}"; + + github.rest.repos.createCommitComment({ + commit_sha: "${{ steps.release_commit.outputs.sha }}", + owner: context.repo.owner, + repo: context.repo.repo, + body: `🎉 ${preid === 'alpha' ? 'Alpha' : 'Beta'} Release!\n\n${packages}`, + }); diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..646e39ee --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,81 @@ +name: Publish +on: + pull_request: + branches: + - 'main' + types: + - closed + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + publish: + if: ${{ (github.head_ref == 'release-stable' || github.head_ref == 'release-beta') && github.event.pull_request.merged == true }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: 1 + + - name: Extract version from lerna.json + run: echo "value=$(jq .version lerna.json -r)" >> $GITHUB_OUTPUT + id: current_version + + - name: Determine Release type + shell: bash + run: echo "value=$([[ ${{ steps.current_version.outputs.value }} =~ beta ]] && echo "beta" || echo "stable")" >> $GITHUB_OUTPUT + id: release_type + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Bump versions + id: bump + run: | + git config --global user.name 'github-actions'; + git config --global user.email 'github-actions@users.noreply.github.com'; + echo ".npmrc" >> .git/info/exclude + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc; + yarn lerna publish from-package --summary-file=${{ runner.temp }}/ --yes + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Get release commit + shell: bash + run: | + COMMIT_HASH=$(git rev-parse --verify HEAD); + echo "sha=$COMMIT_HASH" >> $GITHUB_OUTPUT + id: release_commit + + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const releaseType = "${{ steps.release_type.outputs.value }}"; + const summary = JSON.parse(fs.readFileSync('${{ runner.temp }}/lerna-publish-summary.json', 'utf8')); + const packages = summary.map(pkg => `**${pkg.packageName}@${pkg.version}**\n\`\`\`bash\nyarn add ${pkg.packageName}@${{ steps.branch_name.outputs.name }}\n\`\`\``).join('\n'); + + const body = `🎉 ${releaseType === 'stable' ? "Stable" : "Beta"} Release!\n\n${packages}` + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); + + github.rest.repos.createCommitComment({ + commit_sha: "${{ steps.release_commit.outputs.sha }}", + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 00000000..c0333d32 --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,157 @@ +name: Release PR +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + delete-release-prs: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pullRequests } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: "open", + }); + for (const pr of pullRequests) { + if ( + (pr.head.ref === "release-beta" || pr.head.ref === "release-stable") && + pr.user.login.includes("github-actions") + ) { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + state: "closed", + }); + console.log(`Closed PR #${pr.number}`); + } + } + + create-release-pr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: 1 + ref: main + + - name: Extract version from lerna.json + run: echo "value=$(jq .version lerna.json -r)" >> $GITHUB_OUTPUT + id: current_version + + - name: Determine Release type + run: echo "value=$([[ ${{ steps.current_version.outputs.value }} =~ alpha ]] && echo "beta" || echo "stable")" >> $GITHUB_OUTPUT + id: release_type + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Bump versions + id: bump + run: | + git config --global user.name 'github-actions'; + git config --global user.email 'github-actions@users.noreply.github.com'; + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc; + mkdir ${{ runner.temp }}/main; + export type='${{ steps.release_type.outputs.value }}'; + export branch=release-$type; + echo "RELEASE_BRANCH=$branch" >> $GITHUB_ENV; + git switch -C $branch; + + if [[ $type == 'beta' ]]; then + out=$(yarn -s lerna version prerelease --no-private --conventional-commits --conventional-prerelease --preid=beta --no-git-tag-version --force-publish --yes); + else + out=$(yarn -s lerna version --conventional-commits --conventional-graduate --no-git-tag-version --yes); + fi; + + changes=$(echo "$out" | sed -n '/Changes:/,$p' | sed -n '/^ - .* => .*$/p'); + echo "changes<> $GITHUB_OUTPUT; + echo "$changes" >> $GITHUB_OUTPUT; + echo "EOF" >> $GITHUB_OUTPUT; + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Extract version from lerna.json + run: echo "value=$(jq .version lerna.json -r)" >> $GITHUB_OUTPUT + id: next_version + + - name: Commit changes + run: | + git add .; + git restore --staged .npmrc; + git commit -m "chore: release v${{ steps.next_version.outputs.value }}"; + git push origin $RELEASE_BRANCH --force; + + - name: Get release commit + run: | + COMMIT_HASH=$(git rev-parse --verify HEAD); + COMMIT_MESSAGE=$(git log --format=%B -n 1 $COMMIT_HASH); + echo "sha=$COMMIT_HASH" >> $GITHUB_OUTPUT + echo "head=$RELEASE_BRANCH" >> $GITHUB_OUTPUT + echo "message=$COMMIT_MESSAGE" >> $GITHUB_OUTPUT + id: release_commit + + - name: Find latest stable release + id: latest_stable_release + run: | + echo "tag=$(git tag | grep -v -- "-alpha" | grep -v -- "-beta" | sort -V | tail -2 | head -1)" > $GITHUB_OUTPUT; + + - name: Generate changelog + id: changelog + run: | + # TODO + env: + GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/github-script@v6 + id: pr + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const changes = "${{ steps.bump.outputs.changes }}"; + const type = "${{ steps.release_type.outputs.value }}"; + const head = "${{ steps.release_commit.outputs.head }}"; + const title = "${{ steps.release_commit.outputs.message }}"; + let body = ''; + + body += `This PR will bump the following packages:\n` + body += `\`\`\`\n${changes}\n\`\`\``; + body += `\n\n`; + body += `Please review and test the code, before merging this PR. You can also check the changelog for more details on what has changed.\n\n` + body += `**Warning**: Once you merge this PR, the changes will be published to the npm registry and cannot be undone. Please make sure you are confident about the quality and stability of the code before publishing.` + + const res = await github.rest.pulls.create({ + body, + head, + title, + base: "main", + repo: context.repo.repo, + owner: context.repo.owner, + }); + + const { number } = res.data; + core.setOutput('number', number); + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + labels: [`${type}-release`], + }); diff --git a/.github/workflows/remove-canary-releases.yml b/.github/workflows/remove-canary-releases.yml new file mode 100644 index 00000000..f274f6c7 --- /dev/null +++ b/.github/workflows/remove-canary-releases.yml @@ -0,0 +1,73 @@ +name: Remove Canary Releases +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + remove-canary-releases: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Authenticate with NPM + run: | + echo ".npmrc" >> .git/info/exclude + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Get package names + id: packages + run: | + packages=$(yarn -s lerna list --json --loglevel error | jq '.[]["name"]') + echo "name<> $GITHUB_OUTPUT + echo $packages >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT + + - name: List merged pull requests + id: merged_branches + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const response = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: "closed", + base: "main", + head: "topic-*", + }); + + console.log(response.data.map(pr => pr.head.ref)) + + const prs = response.data + .filter((pr) => !!pr.merged_at) + .filter((pr) => +new Date() - new Date(pr.merged_at) > 24 * 60 * 60 * 1000); + + const branches = prs.map((pr) => pr.head.ref); + core.setOutput('branches', branches.join("\n")); + + - name: Remove canary releases + run: | + packages=$(echo "${{ steps.packages.outputs.name }}" | tr -d '"') + branches=$(echo "${{ steps.merged_branches.outputs.branches }}") + for package in $packages; do + for branch in $branches; do + npm view $package dist-tags.$branch | xargs -i -d '\n' sh -c "npm unpublish $package@{} --force 2>&1 || true" + npm dist-tag rm $package $branch 2>&1 || true + done + done