diff --git a/.gitattributes b/.gitattributes index 4a60c0d0..f7644b0f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,12 +6,15 @@ /.github/pull_request_template.md linguist-generated /.github/workflows/auto-approve.yml linguist-generated /.github/workflows/auto-merge.yml linguist-generated +/.github/workflows/auto-tag-dev-v5.0.yml linguist-generated /.github/workflows/auto-tag-dev.yml linguist-generated +/.github/workflows/auto-tag-releases-v5.0.yml linguist-generated /.github/workflows/auto-tag-releases.yml linguist-generated /.github/workflows/build.yml linguist-generated /.github/workflows/pull-request-lint.yml linguist-generated /.github/workflows/release.yml linguist-generated -/.github/workflows/upgrade.yml linguist-generated +/.github/workflows/upgrade-main.yml linguist-generated +/.github/workflows/upgrade-maintenance-v5.0.yml linguist-generated /.gitignore linguist-generated /.npmignore linguist-generated /.npmrc linguist-generated diff --git a/.github/workflows/auto-tag-dev-v5.0.yml b/.github/workflows/auto-tag-dev-v5.0.yml new file mode 100644 index 00000000..5c0dfa03 --- /dev/null +++ b/.github/workflows/auto-tag-dev-v5.0.yml @@ -0,0 +1,59 @@ +# ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". + +name: auto-tag-dev-v5.0 +run-name: Auto-Tag Prerelease (v5.0) +on: + schedule: + - cron: 0 5 * * 0,2-6 + workflow_dispatch: {} +jobs: + pre-flight: + name: Pre-Flight Checks + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + sha: ${{ steps.git.outputs.sha }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + repository: ${{ github.repository }} + ref: maintenance/v5.0 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version: 14.18.0 + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Build + run: yarn build + - name: Identify git SHA + id: git + run: echo sha=$(git rev-parse HEAD) >> $GITHUB_OUTPUT + auto-tag: + name: Auto-Tag Release + needs: pre-flight + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + repository: ${{ github.repository }} + token: ${{ secrets.PROJEN_GITHUB_TOKEN }} + ref: ${{ needs.pre-flight.outputs.sha }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version: 14.18.0 + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Set git identity + run: |- + git config user.name "github-actions" + git config user.email "github-actions@github.com" + - name: Tag PreRelease + run: yarn tag-release --idempotent --no-sign --push --prerelease=dev diff --git a/.github/workflows/auto-tag-releases-v5.0.yml b/.github/workflows/auto-tag-releases-v5.0.yml new file mode 100644 index 00000000..57170505 --- /dev/null +++ b/.github/workflows/auto-tag-releases-v5.0.yml @@ -0,0 +1,59 @@ +# ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". + +name: auto-tag-releases-v5.0 +run-name: Auto-Tag Release (v5.0) +on: + schedule: + - cron: 0 5 * * 1 + workflow_dispatch: {} +jobs: + pre-flight: + name: Pre-Flight Checks + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + sha: ${{ steps.git.outputs.sha }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + repository: ${{ github.repository }} + ref: maintenance/v5.0 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version: 14.18.0 + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Build + run: yarn build + - name: Identify git SHA + id: git + run: echo sha=$(git rev-parse HEAD) >> $GITHUB_OUTPUT + auto-tag: + name: Auto-Tag Release + needs: pre-flight + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + repository: ${{ github.repository }} + token: ${{ secrets.PROJEN_GITHUB_TOKEN }} + ref: ${{ needs.pre-flight.outputs.sha }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version: 14.18.0 + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Set git identity + run: |- + git config user.name "github-actions" + git config user.email "github-actions@github.com" + - name: Tag Release + run: "yarn tag-release --idempotent --no-sign --push " diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a12d9f55..abcc5b9c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,7 @@ on: push: branches: - main + - maintenance/* jobs: build: runs-on: ubuntu-latest @@ -132,6 +133,16 @@ jobs: node-version: - 16.x - 18.x + matrix-clear: + name: Unit Tests + needs: matrix-test + runs-on: ubuntu-latest + permissions: {} + env: + CI: "true" + steps: + - name: Done + run: echo OK package: name: package needs: build diff --git a/.github/workflows/upgrade.yml b/.github/workflows/upgrade-main.yml similarity index 90% rename from .github/workflows/upgrade.yml rename to .github/workflows/upgrade-main.yml index 8f4ec608..d45e041e 100644 --- a/.github/workflows/upgrade.yml +++ b/.github/workflows/upgrade-main.yml @@ -1,6 +1,6 @@ # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". -name: upgrade +name: upgrade-main on: workflow_dispatch: {} schedule: @@ -16,6 +16,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + ref: main - name: Setup Node.js uses: actions/setup-node@v3 with: @@ -45,7 +47,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - with: {} + with: + ref: main - name: Download patch uses: actions/download-artifact@v3 with: @@ -71,8 +74,8 @@ jobs: ------ - *Automatically created by projen via the "upgrade" workflow* - branch: github-actions/upgrade + *Automatically created by projen via the "upgrade-main" workflow* + branch: github-actions/upgrade-main title: "chore(deps): upgrade dependencies" labels: auto-approve body: |- @@ -82,7 +85,7 @@ jobs: ------ - *Automatically created by projen via the "upgrade" workflow* + *Automatically created by projen via the "upgrade-main" workflow* author: github-actions committer: github-actions signoff: true diff --git a/.github/workflows/upgrade-maintenance-v5.0.yml b/.github/workflows/upgrade-maintenance-v5.0.yml new file mode 100644 index 00000000..d98bc3a5 --- /dev/null +++ b/.github/workflows/upgrade-maintenance-v5.0.yml @@ -0,0 +1,95 @@ +# ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". + +name: upgrade-maintenance-v5.0 +on: + workflow_dispatch: {} + schedule: + - cron: 0 0 * * * +jobs: + upgrade: + name: Upgrade + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + patch_created: ${{ steps.create_patch.outputs.patch_created }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: maintenance/v5.0 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 14.18.0 + - name: Install dependencies + run: yarn install --check-files --frozen-lockfile + - name: Back-port projenrc changes from main + env: + CI: "true" + run: git checkout origin/main -- .projenrc.ts projenrc README.md && yarn projen + - name: Upgrade dependencies + run: npx projen upgrade + - name: Find mutations + id: create_patch + run: |- + git add . + git diff --staged --patch --exit-code > .repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT + - name: Upload patch + if: steps.create_patch.outputs.patch_created + uses: actions/upload-artifact@v3 + with: + name: .repo.patch + path: .repo.patch + pr: + name: Create Pull Request + needs: upgrade + runs-on: ubuntu-latest + permissions: + contents: read + if: ${{ needs.upgrade.outputs.patch_created }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: maintenance/v5.0 + - name: Download patch + uses: actions/download-artifact@v3 + with: + name: .repo.patch + path: ${{ runner.temp }} + - name: Apply patch + run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' + - name: Set git identity + run: |- + git config user.name "github-actions" + git config user.email "github-actions@github.com" + - name: Create Pull Request + id: create-pr + uses: peter-evans/create-pull-request@v4 + with: + token: ${{ secrets.PROJEN_GITHUB_TOKEN }} + commit-message: |- + chore(deps): upgrade dependencies + + Upgrades project dependencies. See details in [workflow run]. + + [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + + ------ + + *Automatically created by projen via the "upgrade-maintenance-v5.0" workflow* + branch: github-actions/upgrade-maintenance-v5.0 + title: "chore(deps): upgrade dependencies" + labels: auto-approve + body: |- + Upgrades project dependencies. See details in [workflow run]. + + [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + + ------ + + *Automatically created by projen via the "upgrade-maintenance-v5.0" workflow* + author: github-actions + committer: github-actions + signoff: true diff --git a/.gitignore b/.gitignore index 2156e79a..7003814d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ jspm_packages/ !/.projenrc.js !/jest.config.json /coverage/ -!/.github/workflows/upgrade.yml !/.github/pull_request_template.md !/.prettierrc.json !/.npmrc @@ -45,6 +44,8 @@ jspm_packages/ /lib /dist/ !/.eslintrc.json +!/.github/workflows/upgrade-main.yml +!/.github/workflows/upgrade-maintenance-v5.0.yml /build-tools/tsconfig.json /projenrc/tsconfig.json /test/tsconfig.json @@ -58,3 +59,5 @@ jspm_packages/ !/.github/workflows/release.yml !/.github/workflows/auto-tag-dev.yml !/.github/workflows/auto-tag-releases.yml +!/.github/workflows/auto-tag-dev-v5.0.yml +!/.github/workflows/auto-tag-releases-v5.0.yml diff --git a/.projen/files.json b/.projen/files.json index a0943a6d..2340c662 100644 --- a/.projen/files.json +++ b/.projen/files.json @@ -5,12 +5,15 @@ ".github/pull_request_template.md", ".github/workflows/auto-approve.yml", ".github/workflows/auto-merge.yml", + ".github/workflows/auto-tag-dev-v5.0.yml", ".github/workflows/auto-tag-dev.yml", + ".github/workflows/auto-tag-releases-v5.0.yml", ".github/workflows/auto-tag-releases.yml", ".github/workflows/build.yml", ".github/workflows/pull-request-lint.yml", ".github/workflows/release.yml", - ".github/workflows/upgrade.yml", + ".github/workflows/upgrade-main.yml", + ".github/workflows/upgrade-maintenance-v5.0.yml", ".gitignore", ".npmignore", ".npmrc", diff --git a/.projen/tasks.json b/.projen/tasks.json index b9b956a4..8653e385 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -243,19 +243,22 @@ "exec": "yarn upgrade npm-check-updates" }, { - "exec": "npm-check-updates --dep dev --upgrade --target=minor --reject='jsii-1.x'" + "exec": "npm-check-updates --dep dev --upgrade --target=minor --reject='jsii-1.x,typescript'" }, { - "exec": "npm-check-updates --dep optional --upgrade --target=minor --reject='jsii-1.x'" + "exec": "npm-check-updates --dep optional --upgrade --target=minor --reject='jsii-1.x,typescript'" }, { - "exec": "npm-check-updates --dep peer --upgrade --target=minor --reject='jsii-1.x'" + "exec": "npm-check-updates --dep peer --upgrade --target=minor --reject='jsii-1.x,typescript'" }, { - "exec": "npm-check-updates --dep prod --upgrade --target=minor --reject='jsii-1.x'" + "exec": "npm-check-updates --dep prod --upgrade --target=minor --reject='jsii-1.x,typescript'" }, { - "exec": "npm-check-updates --dep bundle --upgrade --target=minor --reject='jsii-1.x'" + "exec": "npm-check-updates --dep bundle --upgrade --target=minor --reject='jsii-1.x,typescript'" + }, + { + "exec": "npm-check-updates --upgrade --target=patch --filter=typescript" }, { "exec": "yarn install --check-files" diff --git a/.projenrc.ts b/.projenrc.ts index 54ab2e81..0d7640bc 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -2,8 +2,9 @@ import { javascript, JsonFile, JsonPatch, typescript } from 'projen'; import { BuildWorkflow } from './projenrc/build-workflow'; import { JsiiCalcFixtures } from './projenrc/fixtures'; import { ReleaseWorkflow } from './projenrc/release'; -import { SupportPolicy } from './projenrc/support'; +import { SUPPORT_POLICY, SupportPolicy } from './projenrc/support'; import { UpdateIntegPackage } from './projenrc/update-integ-package'; +import { UpgradeDependencies } from './projenrc/upgrade-dependencies'; const project = new typescript.TypeScriptProject({ projenrcTs: true, @@ -78,16 +79,33 @@ const project = new typescript.TypeScriptProject({ buildWorkflow: false, // We have our own build workflow (need matrix test) release: false, // We have our own release workflow - defaultReleaseBranch: 'release', + defaultReleaseBranch: 'main', autoApproveUpgrades: true, autoApproveOptions: { allowedUsernames: ['aws-cdk-automation', 'github-bot'], }, + depsUpgrade: false, // We have our own custom upgrade workflow + vscode: true, }); +new UpgradeDependencies(project, { + workflowOptions: { + branches: [ + 'main', + ...Object.entries(SUPPORT_POLICY.maintenance).flatMap(([version, until]) => { + if (Date.now() > until.getTime()) { + return []; + } + return [`maintenance/v${version}`]; + }), + ], + labels: ['auto-approve'], + }, +}); + // VSCode will look at the "closest" file named "tsconfig.json" when deciding on which config to use // for a given TypeScript file with the TypeScript language server. In order to make this "seamless" // we'll be dropping `tsconfig.json` files at strategic locations in the project. These will not be @@ -109,9 +127,6 @@ project.tsconfig?.file?.patch( JsonPatch.add('/compilerOptions/declarationMap', true), ); -// Add support policy documents -new SupportPolicy(project); - // Don't show .gitignore'd files in the VSCode explorer project.vscode!.settings.addSetting('explorer.excludeGitIgnore', true); // Use the TypeScript SDK from the project dependencies @@ -214,8 +229,9 @@ new JsiiCalcFixtures(project); // Add Node.js version matrix test new BuildWorkflow(project); -// Add the custom release workflow -new ReleaseWorkflow(project) +// Add support policy documents & release workflows +new SupportPolicy(project); +const releases = new ReleaseWorkflow(project) .autoTag({ preReleaseId: 'dev', runName: 'Auto-Tag Prerelease (default branch)', @@ -226,6 +242,32 @@ new ReleaseWorkflow(project) schedule: '0 0 * * 1', // Mondays at midnight }); +// We'll stagger release schedules so as to avoid everything going out at once. +let hour = 0; +for (const [version, until] of Object.entries(SUPPORT_POLICY.maintenance)) { + if (Date.now() <= until.getTime()) { + // Stagger schedules every 5 hours, rolling. 5 was selected because it's co-prime to 24. + hour = (hour + 5) % 24; + + const branch = `v${version}`; + + releases + .autoTag({ + preReleaseId: 'dev', + runName: `Auto-Tag Prerelease (${branch})`, + schedule: `0 ${hour} * * 0,2-6`, // Tuesday though sundays + branch: `maintenance/${branch}`, + nameSuffix: branch, + }) + .autoTag({ + runName: `Auto-Tag Release (${branch})`, + schedule: `0 ${hour} * * 1`, // Mondays + branch: `maintenance/${branch}`, + nameSuffix: branch, + }); + } +} + new UpdateIntegPackage(project); project.synth(); diff --git a/README.md b/README.md index 99ec2775..98e4d9d9 100644 --- a/README.md +++ b/README.md @@ -287,10 +287,11 @@ The applicable _Maintenance & Support policy_ can be reviewed in [SUPPORT.md](./ The current status of `jsii` compiler releases is: -Release | Status | Comment ---------|---------|----------------------------- -`1.x` | Current | https://github.com/aws/jsii -`5.0.x` | Current | ![](https://img.shields.io/npm/v/jsii/v5.0-latest?label=jsii%40v5.0-latest&logo=npm) +Release | Status | Comment +--------|-------------|----------------------------- +`1.x` | Current | https://github.com/aws/jsii +`5.1.x` | Current | ![](https://img.shields.io/npm/v/jsii/v5.1-latest?label=jsii%40v5.1-latest&logo=npm) +`5.0.x` | Maintenance | ![](https://img.shields.io/npm/v/jsii/v5.0-latest?label=jsii%40v5.0-latest&logo=npm) ## :balance_scale: License diff --git a/projenrc/build-workflow.ts b/projenrc/build-workflow.ts index 5b62f73c..2072e7f4 100644 --- a/projenrc/build-workflow.ts +++ b/projenrc/build-workflow.ts @@ -35,7 +35,7 @@ export class BuildWorkflow { if (opts.defaultBranch !== null) { wf.on({ push: { - branches: [opts.defaultBranch ?? 'main'], + branches: [opts.defaultBranch ?? 'main', 'maintenance/*'], }, }); } @@ -211,6 +211,15 @@ export class BuildWorkflow { }, ], }, + 'matrix-clear': { + // This is a simple "join target" to simplify branch protection rules. + env: { CI: 'true' }, + name: 'Unit Tests', + needs: ['matrix-test'], + permissions: {}, + runsOn: ['ubuntu-latest'], + steps: [{ name: 'Done', run: 'echo OK' }], + }, 'package': { env: { CI: 'true' }, name: 'package', diff --git a/projenrc/release.ts b/projenrc/release.ts index 72d5f78c..eddfb6a4 100644 --- a/projenrc/release.ts +++ b/projenrc/release.ts @@ -244,11 +244,8 @@ export class ReleaseWorkflow { } public autoTag(opts: AutoTagWorkflowProps): this { - new AutoTagWorkflow( - this.project, - `auto-tag-${opts.preReleaseId ?? 'releases'}${opts.branch ? `-${opts.branch}` : ''}`, - opts, - ); + const suffix = opts.nameSuffix ? `-${opts.nameSuffix}` : opts.branch ? `-${opts.branch}` : ''; + new AutoTagWorkflow(this.project, `auto-tag-${opts.preReleaseId ?? 'releases'}${suffix}`, opts); return this; } } @@ -321,6 +318,14 @@ interface AutoTagWorkflowProps { * @default - a regular release will be tagged. */ readonly preReleaseId?: string; + + /** + * The workflow name suffix. A single `-` will be prepended to this value if + * present, which is then appended at the end of the workflow name. + * + * @default - derived from the branch name (if present). + */ + readonly nameSuffix?: string; } class AutoTagWorkflow { diff --git a/projenrc/support.ts b/projenrc/support.ts index 437984f9..ea3873d1 100644 --- a/projenrc/support.ts +++ b/projenrc/support.ts @@ -1,15 +1,19 @@ import { JsonFile, Project } from 'projen'; import type { ReleasesDocument } from '../src/support'; +export const SUPPORT_POLICY: ReleasesDocument = { + current: '5.1', + maintenance: { + '5.0': new Date('2024-01-31'), + }, +}; + export class SupportPolicy { public constructor(project: Project) { new JsonFile(project, 'releases.json', { allowComments: false, editGitignore: false, - obj: { - current: '5.0', - maintenance: {}, - } satisfies ReleasesDocument, + obj: SUPPORT_POLICY, readonly: true, }); } diff --git a/projenrc/upgrade-dependencies.ts b/projenrc/upgrade-dependencies.ts new file mode 100644 index 00000000..203f1891 --- /dev/null +++ b/projenrc/upgrade-dependencies.ts @@ -0,0 +1,451 @@ +import { Component, DependencyType, github, javascript, release, Task, TaskStep } from 'projen'; +import { DEFAULT_GITHUB_ACTIONS_USER } from 'projen/lib/github/constants'; + +const CREATE_PATCH_STEP_ID = 'create_patch'; +const PATCH_CREATED_OUTPUT = 'patch_created'; + +/** + * Options for `UpgradeDependencies`. + */ +export interface UpgradeDependenciesOptions { + /** + * List of package names to exclude during the upgrade. + * + * @default - Nothing is excluded. + */ + readonly exclude?: string[]; + + /** + * List of package names to include during the upgrade. + * + * @default - Everything is included. + */ + readonly include?: string[]; + + /** + * Include a github workflow for creating PR's that upgrades the + * required dependencies, either by manual dispatch, or by a schedule. + * + * If this is `false`, only a local projen task is created, which can be executed manually to + * upgrade the dependencies. + * + * @default - true for root projects, false for sub-projects. + */ + readonly workflow?: boolean; + + /** + * Options for the github workflow. Only applies if `workflow` is true. + * + * @default - default options. + */ + readonly workflowOptions?: UpgradeDependenciesWorkflowOptions; + + /** + * The name of the task that will be created. + * This will also be the workflow name. + * + * @default "upgrade". + */ + readonly taskName?: string; + + /** + * Title of the pull request to use (should be all lower-case). + * + * @default "upgrade dependencies" + */ + readonly pullRequestTitle?: string; + + /** + * Add Signed-off-by line by the committer at the end of the commit log message. + * + * @default true + */ + readonly signoff?: boolean; +} + +/** + * Upgrade node project dependencies. + */ +export class UpgradeDependencies extends Component { + /** + * The workflows that execute the upgrades. One workflow per branch. + */ + public readonly workflows: github.GithubWorkflow[] = []; + + private readonly options: UpgradeDependenciesOptions; + private readonly _project: javascript.NodeProject; + private readonly pullRequestTitle: string; + + /** + * Container definitions for the upgrade workflow. + */ + public containerOptions?: github.workflows.ContainerOptions; + + /** + * The upgrade task. + */ + public readonly upgradeTask: Task; + + /** + * A task run after the upgrade task. + */ + public readonly postUpgradeTask: Task; + + private readonly gitIdentity: github.GitIdentity; + private readonly postBuildSteps: github.workflows.JobStep[]; + private readonly permissions: github.workflows.JobPermissions; + + constructor(project: javascript.NodeProject, options: UpgradeDependenciesOptions = {}) { + super(project); + + this._project = project; + this.options = options; + this.pullRequestTitle = options.pullRequestTitle ?? 'upgrade dependencies'; + this.gitIdentity = options.workflowOptions?.gitIdentity ?? DEFAULT_GITHUB_ACTIONS_USER; + this.permissions = { + contents: github.workflows.JobPermission.READ, + ...options.workflowOptions?.permissions, + }; + this.postBuildSteps = []; + this.containerOptions = options.workflowOptions?.container; + project.addDevDeps('npm-check-updates@^16'); + + this.postUpgradeTask = + project.tasks.tryFind('post-upgrade') ?? + project.tasks.addTask('post-upgrade', { + description: 'Runs after upgrading dependencies', + }); + + this.upgradeTask = project.addTask(options.taskName ?? 'upgrade', { + // this task should not run in CI mode because its designed to + // update package.json and lock files. + env: { CI: '0' }, + description: this.pullRequestTitle, + steps: { toJSON: () => this.renderTaskSteps() } as any, + }); + this.upgradeTask.lock(); // this task is a lazy value, so make it readonly + + if (this.upgradeTask && project.github && (options.workflow ?? true)) { + if (options.workflowOptions?.branches) { + for (const branch of options.workflowOptions.branches) { + this.workflows.push(this.createWorkflow(this.upgradeTask, project.github, branch)); + } + } else if (release.Release.of(project)) { + const rel = release.Release.of(project)!; + rel._forEachBranch((branch: string) => { + this.workflows.push(this.createWorkflow(this.upgradeTask, project.github!, branch)); + }); + } else { + // represents the default repository branch. + // just like not specifying anything. + const defaultBranch = undefined; + this.workflows.push(this.createWorkflow(this.upgradeTask, project.github, defaultBranch)); + } + } + } + + /** + * Add steps to execute a successful build. + * @param steps workflow steps + */ + public addPostBuildSteps(...steps: github.workflows.JobStep[]) { + this.postBuildSteps.push(...steps); + } + + private renderTaskSteps(): TaskStep[] { + const exclude = this.options.exclude ?? []; + + // exclude depedencies that has already version pinned (fully or with patch version) by Projen with ncu (but not package manager upgrade) + // Getting only unique values through set + const ncuExcludes = [ + ...new Set( + this.project.deps.all + .filter( + (dep) => + dep.name === 'typescript' || + (dep.version && dep.version[0] !== '^' && dep.type !== DependencyType.OVERRIDE), + ) + .map((dep) => dep.name) + .concat(exclude), + ), + ]; + // TypeScript is minor-pinned in this project... + const hasTypescript = ncuExcludes.includes('typescript'); + + const ncuIncludes = this.options.include?.filter((item) => !ncuExcludes.includes(item)); + + const includeLength = this.options.include?.length ?? 0; + const ncuIncludesLength = ncuIncludes?.length ?? 0; + + // If all explicit includes already have version pinned, don't add task. + // Note that without explicit includes task gets added + if (includeLength > 0 && ncuIncludesLength === 0) { + return [{ exec: 'echo No dependencies to upgrade.' }]; + } + + const steps = new Array(); + + // update npm-check-updates before everything else, in case there is a bug + // in it or one of its dependencies. This will make upgrade workflows + // slightly more stable and resilient to upstream changes. + steps.push({ + exec: this._project.package.renderUpgradePackagesCommand([], ['npm-check-updates']), + }); + + for (const dep of ['dev', 'optional', 'peer', 'prod', 'bundle']) { + const ncuCommand = ['npm-check-updates', '--dep', dep, '--upgrade', '--target=minor']; + // Don't add includes and excludes same time + if (ncuIncludes) { + ncuCommand.push(`--filter='${ncuIncludes.join(',')}'`); + } else if (ncuExcludes.length > 0) { + ncuCommand.push(`--reject='${ncuExcludes.join(',')}'`); + } + + steps.push({ exec: ncuCommand.join(' ') }); + } + if (hasTypescript) { + const ncuCommand = ['npm-check-updates', '--upgrade', '--target=patch', '--filter=typescript']; + steps.push({ exec: ncuCommand.join(' ') }); + } + + // run "yarn/npm install" to update the lockfile and install any deps (such as projen) + steps.push({ exec: this._project.package.installAndUpdateLockfileCommand }); + + // run upgrade command to upgrade transitive deps as well + steps.push({ + exec: this._project.package.renderUpgradePackagesCommand(exclude, this.options.include), + }); + + // run "projen" to give projen a chance to update dependencies (it will also run "yarn install") + steps.push({ exec: this._project.projenCommand }); + + steps.push({ spawn: this.postUpgradeTask.name }); + + return steps; + } + + private createWorkflow(task: Task, gh: github.GitHub, branch?: string): github.GithubWorkflow { + const schedule = this.options.workflowOptions?.schedule ?? UpgradeDependenciesSchedule.DAILY; + + const workflowName = `${task.name}${branch ? `-${branch.replace(/\//g, '-')}` : ''}`; + const workflow = gh.addWorkflow(workflowName); + const triggers: github.workflows.Triggers = { + workflowDispatch: {}, + schedule: schedule.cron.length > 0 ? schedule.cron.map((e) => ({ cron: e })) : undefined, + }; + workflow.on(triggers); + + const upgrade = this.createUpgrade(task, gh, branch); + const pr = this.createPr(workflow, upgrade); + + const jobs: Record = {}; + jobs[upgrade.jobId] = upgrade.job; + jobs[pr.jobId] = pr.job; + + workflow.addJobs(jobs); + return workflow; + } + + private createUpgrade(task: Task, gh: github.GitHub, branch?: string): Upgrade { + const runsOn = this.options.workflowOptions?.runsOn ?? ['ubuntu-latest']; + + const with_ = { + ...(branch ? { ref: branch } : {}), + ...(gh.downloadLfs ? { lfs: true } : {}), + }; + + const steps: github.workflows.JobStep[] = [ + { + name: 'Checkout', + uses: 'actions/checkout@v3', + with: Object.keys(with_).length > 0 ? with_ : undefined, + }, + ...this._project.renderWorkflowSetup({ mutable: false }), + ...(branch && branch !== 'main' + ? [ + { + env: { CI: 'true' }, + name: 'Back-port projenrc changes from main', + run: 'git checkout origin/main -- .projenrc.ts projenrc README.md && yarn projen', + }, + ] + : []), + { + name: 'Upgrade dependencies', + run: this._project.runTaskCommand(task), + }, + ]; + + steps.push(...this.postBuildSteps); + steps.push( + ...github.WorkflowActions.uploadGitPatch({ + stepId: CREATE_PATCH_STEP_ID, + outputName: PATCH_CREATED_OUTPUT, + }), + ); + + return { + job: { + name: 'Upgrade', + container: this.containerOptions, + permissions: this.permissions, + runsOn: runsOn ?? ['ubuntu-latest'], + steps: steps, + outputs: { + [PATCH_CREATED_OUTPUT]: { + stepId: CREATE_PATCH_STEP_ID, + outputName: PATCH_CREATED_OUTPUT, + }, + }, + }, + jobId: 'upgrade', + ref: branch, + }; + } + + private createPr(workflow: github.GithubWorkflow, upgrade: Upgrade): PR { + const credentials = this.options.workflowOptions?.projenCredentials ?? workflow.projenCredentials; + + return { + job: github.WorkflowJobs.pullRequestFromPatch({ + patch: { + jobId: upgrade.jobId, + outputName: PATCH_CREATED_OUTPUT, + ref: upgrade.ref, + }, + workflowName: workflow.name, + credentials, + runsOn: this.options.workflowOptions?.runsOn, + pullRequestTitle: `chore(deps): ${this.pullRequestTitle}`, + pullRequestDescription: 'Upgrades project dependencies.', + gitIdentity: this.gitIdentity, + assignees: this.options.workflowOptions?.assignees, + labels: this.options.workflowOptions?.labels, + signoff: this.options.signoff, + }), + jobId: 'pr', + }; + } +} + +interface Upgrade { + readonly ref?: string; + readonly job: github.workflows.Job; + readonly jobId: string; +} + +interface PR { + readonly job: github.workflows.Job; + readonly jobId: string; +} + +/** + * Options for `UpgradeDependencies.workflowOptions`. + */ +export interface UpgradeDependenciesWorkflowOptions { + /** + * Schedule to run on. + * + * @default UpgradeDependenciesSchedule.DAILY + */ + readonly schedule?: UpgradeDependenciesSchedule; + + /** + * Choose a method for authenticating with GitHub for creating the PR. + * + * When using the default github token, PR's created by this workflow + * will not trigger any subsequent workflows (i.e the build workflow), so + * projen requires API access to be provided through e.g. a personal + * access token or other method. + * + * @see https://github.com/peter-evans/create-pull-request/issues/48 + * @default - personal access token named PROJEN_GITHUB_TOKEN + */ + readonly projenCredentials?: github.GithubCredentials; + + /** + * Labels to apply on the PR. + * + * @default - no labels. + */ + readonly labels?: string[]; + + /** + * Assignees to add on the PR. + * + * @default - no assignees + */ + readonly assignees?: string[]; + + /** + * Job container options. + * + * @default - defaults + */ + readonly container?: github.workflows.ContainerOptions; + + /** + * List of branches to create PR's for. + * + * @default - All release branches configured for the project. + */ + readonly branches?: string[]; + + /** + * The git identity to use for commits. + * @default "github-actions@github.com" + */ + readonly gitIdentity?: github.GitIdentity; + + /** + * Github Runner selection labels + * @default ["ubuntu-latest"] + */ + readonly runsOn?: string[]; + + /** + * Permissions granted to the upgrade job + * To limit job permissions for `contents`, the desired permissions have to be explicitly set, e.g.: `{ contents: JobPermission.NONE }` + * @default `{ contents: JobPermission.READ }` + */ + readonly permissions?: github.workflows.JobPermissions; +} + +/** + * How often to check for new versions and raise pull requests for version upgrades. + */ +export class UpgradeDependenciesSchedule { + /** + * Disables automatic upgrades. + */ + public static readonly NEVER = new UpgradeDependenciesSchedule([]); + + /** + * At 00:00. + */ + public static readonly DAILY = new UpgradeDependenciesSchedule(['0 0 * * *']); + + /** + * At 00:00 on every day-of-week from Monday through Friday. + */ + public static readonly WEEKDAY = new UpgradeDependenciesSchedule(['0 0 * * 1-5']); + + /** + * At 00:00 on Monday. + */ + public static readonly WEEKLY = new UpgradeDependenciesSchedule(['0 0 * * 1']); + + /** + * At 00:00 on day-of-month 1. + */ + public static readonly MONTHLY = new UpgradeDependenciesSchedule(['0 0 1 * *']); + + /** + * Create a schedule from a raw cron expression. + */ + public static expressions(cron: string[]) { + return new UpgradeDependenciesSchedule(cron); + } + + private constructor(public readonly cron: string[]) {} +} diff --git a/releases.json b/releases.json index 165b805c..bcb034f2 100644 --- a/releases.json +++ b/releases.json @@ -1,5 +1,7 @@ { - "current": "5.0", - "maintenance": {}, + "current": "5.1", + "maintenance": { + "5.0": "2024-01-31T00:00:00.000Z" + }, "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." }