diff --git a/.github/workflows/postrelease.yml b/.github/workflows/postrelease.yml deleted file mode 100644 index f865eebcda..0000000000 --- a/.github/workflows/postrelease.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: 🕊 Post-release - -on: - push: - tags: - # only run on `react-router` tags - - "react-router@*" - -jobs: - comment: - name: 📝 Comment on related issues and pull requests - if: github.repository == 'remix-run/react-router' - uses: ./.github/workflows/release-comments.yml - with: - ref: ${{ github.ref }} - # this should match the above tag to watch excluding the trailing "@" - packageVersionToFollow: "react-router" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 46f630529a..bcc2ac404b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: 🕊 Release +name: 🦋 Changesets Release on: push: branches: @@ -6,34 +6,34 @@ on: - "release-*" - "!release-experimental" - "!release-experimental-*" - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -env: - CI: true + - "!release-manual" + - "!release-manual-*" jobs: release: name: 🦋 Changesets Release if: github.repository == 'remix-run/react-router' runs-on: ubuntu-latest - + outputs: + publishedPackages: ${{ steps.changesets.outputs.publishedPackages }} + published: ${{ steps.changesets.outputs.published }} steps: + - name: 🛑 Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + - name: ⬇️ Checkout repo uses: actions/checkout@v3 with: fetch-depth: 0 - - name: ⎔ Setup Node + - name: ⎔ Setup node uses: actions/setup-node@v3 with: node-version-file: ".nvmrc" - cache: yarn + cache: "yarn" - - name: 📥 Install dependencies - # even though this is called "npm-install" it does use yarn to install - # because we have a yarn.lock and caches efficiently. - uses: bahmutov/npm-install@v1 + - name: 📥 Install deps + run: yarn --frozen-lockfile - name: 🔐 Setup npm auth run: | @@ -52,16 +52,45 @@ jobs: version: yarn run version commit: "chore: Update version for release" title: "chore: Update version for release" - publish: yarn release + publish: yarn run release createGithubReleases: false env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN_SO_OTHER_ACTIONS_RUN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - # comment: - # needs: [release] - # name: 📝 Comment on related issues and pull requests - # if: github.repository == 'remix-run/react-router' - # uses: remix-run/react-router/.github/workflows/release-comments.yml@main - # with: - # ref: ${{ github.ref }} + findPackage: + name: 🦋 Find Package + needs: [release] + runs-on: ubuntu-latest + if: github.repository == 'remix-run/react-router' && needs.release.outputs.published == 'true' + outputs: + package: ${{ steps.findPackage.outputs.package }} + steps: + - name: 🛑 Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + + - name: ⎔ Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: "npm" + + - id: findPackage + run: | + package=$(node ./scripts/release/find-release-from-changeset.js) + echo "package=${package}" >> $GITHUB_OUTPUT + env: + packageVersionToFollow: "react-router" + publishedPackages: ${{ needs.release.outputs.publishedPackages }} + + comment: + name: 📝 Comment on related issues and pull requests + if: github.repository == 'remix-run/react-router' && needs.findPackage.outputs.package != '' + needs: [release, findPackage] + uses: ./.github/workflows/release-comments.yml + with: + ref: refs/tags/${{ needs.findPackage.outputs.package }} + packageVersionToFollow: "react-router" diff --git a/scripts/release/comment.ts b/scripts/release/comment.ts index f8ef8f5f2e..84aad5e789 100644 --- a/scripts/release/comment.ts +++ b/scripts/release/comment.ts @@ -3,7 +3,7 @@ import { OWNER, REPO, PR_FILES_STARTS_WITH, - IS_NIGHTLY_RELEASE, + IS_STABLE_RELEASE, AWAITING_RELEASE_LABEL, } from "./constants"; import { @@ -53,7 +53,7 @@ async function commentOnIssuesAndPrsAboutRelease() { let prLabels = pr.labels.map((label) => label.name); let prIsAwaitingRelease = prLabels.includes(AWAITING_RELEASE_LABEL); - if (!IS_NIGHTLY_RELEASE && prIsAwaitingRelease) { + if (IS_STABLE_RELEASE && prIsAwaitingRelease) { promises.push( removeLabel({ owner: OWNER, repo: REPO, issue: pr.number }) ); @@ -73,27 +73,19 @@ async function commentOnIssuesAndPrsAboutRelease() { issuesCommentedOn.add(issue.number); let issueUrl = getGitHubUrl("issue", issue.number); + console.log(`commenting on issue ${issueUrl}`); - if (IS_NIGHTLY_RELEASE || !prIsAwaitingRelease) { - console.log(`commenting on ${issueUrl}`); - promises.push( - commentOnIssue({ - owner: OWNER, - repo: REPO, - issue: issue.number, - version: VERSION, - }) - ); - } else { - console.log(`commenting on and closing ${issueUrl}`); - promises.push( - commentOnIssue({ - owner: OWNER, - repo: REPO, - issue: issue.number, - version: VERSION, - }) - ); + promises.push( + commentOnIssue({ + owner: OWNER, + repo: REPO, + issue: issue.number, + version: VERSION, + }) + ); + + if (IS_STABLE_RELEASE) { + console.log(`closing issue ${issueUrl}`); promises.push( closeIssue({ owner: OWNER, repo: REPO, issue: issue.number }) ); @@ -104,10 +96,7 @@ async function commentOnIssuesAndPrsAboutRelease() { let result = await Promise.allSettled(promises); let rejected = result.filter((r) => r.status === "rejected"); if (rejected.length > 0) { - console.log( - "🚨 failed to comment on some issues/prs - the most likely reason is they were issues that were turned into discussions, which don't have an api to comment with" - ); - console.log(rejected); + console.error("🚨 failed to comment on some issues/prs", rejected); } } diff --git a/scripts/release/constants.ts b/scripts/release/constants.ts index d78288f362..68a58fca1c 100644 --- a/scripts/release/constants.ts +++ b/scripts/release/constants.ts @@ -1,4 +1,4 @@ -import { cleanupRef, cleanupTagName, isNightly } from "./utils"; +import { cleanupRef, cleanupTagName, isNightly, isStable } from "./utils"; if (!process.env.DEFAULT_BRANCH) { throw new Error("DEFAULT_BRANCH is required"); @@ -32,3 +32,4 @@ export const NIGHTLY_BRANCH = process.env.NIGHTLY_BRANCH; export const PR_FILES_STARTS_WITH = ["packages/"]; export const IS_NIGHTLY_RELEASE = isNightly(VERSION); export const AWAITING_RELEASE_LABEL = "awaiting release"; +export const IS_STABLE_RELEASE = isStable(VERSION); diff --git a/scripts/release/find-release-from-changeset.js b/scripts/release/find-release-from-changeset.js new file mode 100644 index 0000000000..ea0cb529e7 --- /dev/null +++ b/scripts/release/find-release-from-changeset.js @@ -0,0 +1,38 @@ +/** + * + * @param {string | undefined} publishedPackages + * @param {string | undefined} packageVersionToFollow + * @returns {string | undefined} + */ +function findReleaseFromChangeset(publishedPackages, packageVersionToFollow) { + if (!publishedPackages) { + throw new Error("No published packages found"); + } + + let packages = JSON.parse(publishedPackages); + + if (!Array.isArray(packages)) { + throw new Error("Published packages is not an array"); + } + + /** @see https://github.com/changesets/action#outputs */ + /** @type { { name: string; version: string }[] } */ + let typed = packages.filter((pkg) => "name" in pkg && "version" in pkg); + + let found = typed.find((pkg) => pkg.name === packageVersionToFollow); + + if (!found) { + throw new Error( + `${packageVersionToFollow} was not found in the published packages` + ); + } + + let result = `${found.name}@${found.version}`; + console.log(result); + return result; +} + +findReleaseFromChangeset( + process.env.publishedPackages, + process.env.packageVersionToFollow +); diff --git a/scripts/release/github.ts b/scripts/release/github.ts index 1b685e9654..565555ba1d 100644 --- a/scripts/release/github.ts +++ b/scripts/release/github.ts @@ -7,9 +7,12 @@ import { DEFAULT_BRANCH, PACKAGE_VERSION_TO_FOLLOW, AWAITING_RELEASE_LABEL, + IS_NIGHTLY_RELEASE, + IS_STABLE_RELEASE, } from "./constants"; import { gql, graphqlWithAuth, octokit } from "./octokit"; import type { MinimalTag } from "./utils"; +import { isNightly, isStable } from "./utils"; import { cleanupTagName } from "./utils"; import { checkIfStringStartsWith } from "./utils"; @@ -140,34 +143,32 @@ function getPreviousTagFromCurrentTag( return { tag: tagName, date, isPrerelease }; }) - .filter((v: any): v is MinimalTag => typeof v !== "undefined"); + .filter((v: any): v is MinimalTag => typeof v !== "undefined") + .filter((tag) => { + if (IS_STABLE_RELEASE) return isStable(tag.tag); + let isNightlyTag = isNightly(tag.tag); + if (IS_NIGHTLY_RELEASE) return isNightlyTag; + return !isNightlyTag; + }) + .sort((a, b) => { + if (IS_NIGHTLY_RELEASE) { + return b.date.getTime() - a.date.getTime(); + } + + return semver.rcompare(a.tag, b.tag); + }); let currentTagIndex = validTags.findIndex((tag) => tag.tag === currentTag); let currentTagInfo: MinimalTag | undefined = validTags.at(currentTagIndex); let previousTagInfo: MinimalTag | undefined; if (!currentTagInfo) { - throw new Error(`Could not find last tag ${currentTag}`); - } - - // if the currentTag was a stable tag, then we want to find the previous stable tag - if (!currentTagInfo.isPrerelease) { - validTags = validTags - .filter((tag) => !tag.isPrerelease) - .sort((a, b) => semver.rcompare(a.tag, b.tag)); - - currentTagIndex = validTags.findIndex((tag) => tag.tag === currentTag); - currentTagInfo = validTags.at(currentTagIndex); - if (!currentTagInfo) { - throw new Error(`Could not find last stable tag ${currentTag}`); - } + throw new Error(`Could not find tag ${currentTag}`); } previousTagInfo = validTags.at(currentTagIndex + 1); if (!previousTagInfo) { - throw new Error( - `Could not find previous prerelease tag from ${currentTag}` - ); + throw new Error(`Could not find previous tag from ${currentTag}`); } return { @@ -232,21 +233,35 @@ interface GitHubGraphqlTag { interface GitHubGraphqlTagResponse { repository: { refs: { + pageInfo: { + hasNextPage: boolean; + endCursor: string; + }; nodes: Array; }; }; } -async function getTags(owner: string, repo: string) { +async function getTags( + owner: string, + repo: string, + endCursor?: string, + nodes: Array = [] +): Promise { let response: GitHubGraphqlTagResponse = await graphqlWithAuth( gql` - query GET_TAGS($owner: String!, $repo: String!) { + query GET_TAGS($owner: String!, $repo: String!, $endCursor: String) { repository(owner: $owner, name: $repo) { refs( refPrefix: "refs/tags/" first: 100 orderBy: { field: TAG_COMMIT_DATE, direction: DESC } + after: $endCursor ) { + pageInfo { + hasNextPage + endCursor + } nodes { name target { @@ -267,15 +282,26 @@ async function getTags(owner: string, repo: string) { } } `, - { owner, repo } + { owner, repo, endCursor } ); - return response.repository.refs.nodes.filter((node) => { + let filtered = response.repository.refs.nodes.filter((node) => { return ( node.name.startsWith(PACKAGE_VERSION_TO_FOLLOW) || node.name.startsWith("v0.0.0-nightly-") ); }); + + if (response.repository.refs.pageInfo.hasNextPage) { + console.log("has next page", response.repository.refs.pageInfo.endCursor); + + return getTags(owner, repo, response.repository.refs.pageInfo.endCursor, [ + ...nodes, + ...filtered, + ]); + } + + return [...nodes, ...filtered]; } export async function getIssuesClosedByPullRequests( diff --git a/scripts/release/utils.ts b/scripts/release/utils.ts index ea6a86f7b4..a84a60673c 100644 --- a/scripts/release/utils.ts +++ b/scripts/release/utils.ts @@ -1,3 +1,5 @@ +import * as semver from "semver"; + import { GITHUB_REPOSITORY, PACKAGE_VERSION_TO_FOLLOW } from "./constants"; export function checkIfStringStartsWith( @@ -34,3 +36,7 @@ export function cleanupRef(ref: string) { export function isNightly(tagName: string) { return tagName.startsWith("v0.0.0-nightly-"); } + +export function isStable(tagName: string) { + return semver.prerelease(tagName) === null; +}