diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a199f2ea66..ef55fb0f95 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,28 +1,29 @@ - - + ## Description of your changes - + +Please include also relevant motivation and context. - +Add any applicable code snippets, links, screenshots, or other resources +that can help us verify your changes. +--> - - -### How to verify this change - - - +### Related issues, RFCs - - + - +Note: If no issue is present the PR might get blocked and not be reviewed. +--> **Issue number:** ## Checklist @@ -31,12 +32,8 @@ - [ ] I have performed a *self-review* of my own code - [ ] I have *commented* my code where necessary, particularly in areas that should be flagged with a TODO, or hard-to-understand areas - [ ] I have made corresponding changes to the *documentation* -- [ ] I have made corresponding changes to the *examples* - [ ] My changes generate *no new warnings* -- [ ] The *code coverage* hasn't decreased - [ ] I have *added tests* that prove my change is effective and works -- [ ] New and existing *unit tests pass* locally and in Github Actions -- [ ] Any *dependent changes have been merged and published* - [ ] The PR title follows the [conventional commit semantics](https://github.com/awslabs/aws-lambda-powertools-typescript/blob/main/.github/semantic.yml#L2) ### Breaking change checklist diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml new file mode 100644 index 0000000000..d19858b927 --- /dev/null +++ b/.github/boring-cyborg.yml @@ -0,0 +1,173 @@ +##### Labeler ########################################################################################################## +labelPRBasedOnFilePath: + area/logger: + - packages/logger/src/* + - packages/logger/src/**/* + area/tracer: + - packages/tracer/src/* + - packages/tracer/src/**/* + area/metrics: + - packages/metrics/src/* + - packages/metrics/src/**/* + area/idempotency: + - packages/idempotency/src/* + - packages/idempotency/src/**/* + area/parameters: + - packages/parameters/src/* + - packages/parameters/src/**/* + area/parser: + - packages/parser/src/* + - packages/parser/src/**/* + area/validator: + - packages/validator/src/* + - packages/validator/src/**/* + area/batch: + - packages/batch/src/* + - packages/batch/src/**/* + area/commons: + - packages/commons/src/* + - packages/commons/src/**/* + + area/layers: + - layers/src/* + - layers/src/**/* + - layers/bin/* + - layers/cdk.json + + type/tests: + - packages/logger/tests/* + - packages/logger/tests/**/* + - packages/logger/jest.config.js + - packages/tracer/tests/* + - packages/tracer/tests/**/* + - packages/tracer/jest.config.js + - packages/metrics/tests/* + - packages/metrics/tests/**/* + - packages/metrics/jest.config.js + - packages/idempotency/tests/* + - packages/idempotency/tests/**/* + - packages/idempotency/jest.config.js + - packages/parameters/tests/* + - packages/parameters/tests/**/* + - packages/parameters/jest.config.js + - packages/parser/tests/* + - packages/parser/tests/**/* + - packages/parser/jest.config.js + - packages/validator/tests/* + - packages/validator/tests/**/* + - packages/validator/jest.config.js + - packages/batch/tests/* + - packages/batch/tests/**/* + - packages/batch/jest.config.js + - packages/commons/tests/* + - packages/commons/tests/**/* + - packages/commons/jest.config.js + - layers/jest.config.js + - layers/tests/* + - layers/tests/**/* + + area/documentation: + - docs/* + - docs/**/* + - mkdocs.yml + - typedoc.js + - examples/cdk/bin/* + - examples/cdk/functions/* + - examples/cdk/functions/**/* + - examples/cdk/src/* + - examples/cdk/src/**/* + - examples/cdk/tests/* + - examples/cdk/tests/**/* + - examples/cdk/README.md + - examples/cdk/cdk.json + - examples/sam/events/* + - examples/sam/src/* + - examples/sam/src/**/* + - examples/sam/tests/* + - examples/sam/tests/**/* + - examples/sam/README.md + - examples/sam/template.yaml + + area/automation: + - .github/scripts/* + - .github/scripts/**/* + - .github/workflows/* + - .github/workflows/**/* + - .github/actions/* + - .github/actions/**/* + - .github/release-drafter.yml + - .github/boring-cyborg.yml + - .github/semantic.yml + + type/internal: + - .github/CODEOWNERS + - .github/PULL_REQUEST_TEMPLATE.md + - .github/ISSUE_TEMPLATE/* + - CONTRIBUTING.md + - MAINTAINERS.md + - CODE_OF_CONDUCT.md + - SECURITY.md + - LICENSE + - LICENSE-THIRD-PARTY + - lerna.json + - .nvmrc + - .eslintrc.js + - .eslintignore + - .npmignore + - .gitpod.yml + - .husky/* + - .devcontainer/* + - packages/logger/tsconfig*.json + - packages/logger/README.md + - packages/metrics/tsconfig*.json + - packages/metrics/README.md + - packages/tracer/tsconfig*.json + - packages/tracer/README.md + - packages/parser/tsconfig*.json + - packages/parser/README.md + - packages/idempotency/tsconfig*.json + - packages/idempotency/README.md + - packages/batch/tsconfig*.json + - packages/batch/README.md + - packages/commons/tsconfig*.json + - packages/commons/README.md + - packages/validator/tsconfig*.json + - packages/validator/README.md + - packages/parser/tsconfig*.json + - packages/parser/README.md + - layers/tsconfig*.json + - layers/README.md + - examples/sam/tsconfig*.json + - examples/cdk/tsconfig*.json + + type/dependencies: + - package.json + - package-lock.json + - packages/tracer/package.json + - packages/metrics/package.json + - packages/logger/package.json + - packages/commons/package.json + - packages/parameters/package.json + - packages/idempotency/package.json + - packages/parser/package.json + - packages/validator/package.json + - packages/batch/package.json + - layers/package.json + - examples/cdk/package.json + - examples/sam/package.json + +##### Greetings ######################################################################################################## +firstPRWelcomeComment: > + Thanks a lot for your first contribution! Please check out our [contributing guidelines](https://github.com/awslabs/aws-lambda-powertools-typescript/blob/chore/pr_automation/CONTRIBUTING.md) and don't hesitate to ask whatever you need. + + In the meantime, check out the #typescript channel on our AWS Lambda Powertools Discord: [Invite link](https://discord.gg/B8zZKbbyET) + +# Comment to be posted to congratulate user on their first merged PR +firstPRMergeComment: > + Awesome work, congrats on your first merged pull request and thank you for helping improve everyone's experience! + +# Comment to be posted to on first time issues +firstIssueWelcomeComment: > + Thanks for opening your first issue here! We'll come back to you as soon as we can. + + In the meantime, check out the #typescript channel on our AWS Lambda Powertools Discord: [Invite link](https://discord.gg/B8zZKbbyET) diff --git a/.github/scripts/constants.js b/.github/scripts/constants.js index 2e3cb78f91..e74a17928d 100644 --- a/.github/scripts/constants.js +++ b/.github/scripts/constants.js @@ -36,10 +36,6 @@ module.exports = Object.freeze({ /** @type {string[]} */ "IGNORE_AUTHORS": ["dependabot[bot]"], - /** @type {string[]} */ - "AREAS": [ - "tracer", - "metrics", - "logger", - ], + /** @type {RegExp} */ + "RELATED_ISSUE_REGEX": /\bIssue number:(?:closes?|close|fix|fixes?|fixed|resolves?|resolved)?\s*#(?\d+)\b/ }); \ No newline at end of file diff --git a/.github/scripts/label_missing_related_issue.js b/.github/scripts/label_missing_related_issue.js index 53400935f2..f5def29266 100644 --- a/.github/scripts/label_missing_related_issue.js +++ b/.github/scripts/label_missing_related_issue.js @@ -5,36 +5,50 @@ const { PR_NUMBER, IGNORE_AUTHORS, LABEL_BLOCK, - LABEL_BLOCK_REASON + LABEL_BLOCK_REASON, + RELATED_ISSUE_REGEX, } = require("./constants"); -module.exports = async ({github, context, core}) => { - if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { - return core.notice("Author in IGNORE_AUTHORS list; skipping..."); - } +module.exports = async ({ github, context, core }) => { + if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { + return core.notice("Author in IGNORE_AUTHORS list; skipping..."); + } - if (PR_ACTION != "opened") { - return core.notice("Only newly open PRs are labelled to avoid spam; skipping"); - } + if (["opened"].includes(PR_ACTION)) { + return core.notice( + "Only newly opened PRs are labelled to avoid spam; skipping" + ); + } - const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; - const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); - if (isMatch == null) { - core.info(`No related issue found, maybe the author didn't use the template but there is one.`); + const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); + if (isMatch == null) { + core.info( + `No related issue found, maybe the author didn't use the template but there is one.` + ); - let msg = "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure."; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - body: msg, - issue_number: PR_NUMBER, - }); + let msg = + "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure."; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + body: msg, + issue_number: PR_NUMBER, + }); - return await github.rest.issues.addLabels({ - issue_number: PR_NUMBER, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [LABEL_BLOCK, LABEL_BLOCK_REASON] - }); - } -} \ No newline at end of file + return await github.rest.issues.addLabels({ + issue_number: PR_NUMBER, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [LABEL_BLOCK, LABEL_BLOCK_REASON], + }); + } else { + const { closingWord, issue } = isMatch.groups; + core.info( + `Found related issue #${issue} ${ + closingWord === undefined + ? "without closing word" + : `with closing word ${closingWord}` + }` + ); + } +}; diff --git a/.github/scripts/label_pr_based_on_title.js b/.github/scripts/label_pr_based_on_title.js index 12465eab1a..24e17191b9 100644 --- a/.github/scripts/label_pr_based_on_title.js +++ b/.github/scripts/label_pr_based_on_title.js @@ -1,45 +1,45 @@ const { PR_NUMBER, PR_TITLE } = require("./constants"); -module.exports = async ({github, context, core}) => { - const BUG_REGEX = /(fix|bug)(\((.+)\))?(:.+)/ - const FEAT_REFACTOR_REGEX = /(feat|refactor)(\((.+)\))?(:.+)/ - const DOCS_REGEX = /(docs|doc)(\((.+)\))?(:.+)/ - const CHORE_REGEX = /(chore)(\((.+)\))?(:.+)/ - const DEPRECATED_REGEX = /(deprecated)(\((.+)\))?(:.+)/ - - const labels = { - "type/feature": FEAT_REFACTOR_REGEX, - "type/bug": BUG_REGEX, - "area/documentation": DOCS_REGEX, - "type/internal": CHORE_REGEX, - "type/deprecation": DEPRECATED_REGEX, - } +module.exports = async ({ github, context, core }) => { + const BUG_REGEX = /(fix|bug)(\((.+)\))?(:.+)/; + const ENHANCEMENT_REGEX = /(refactor|improv)(\((.+)\))?(:.+)/; + const FEAT_REFACTOR_REGEX = /(feat)(\((.+)\))?(:.+)/; + const DEPRECATED_REGEX = /(deprecated)(\((.+)\))?(:.+)/; + + const labels = { + "type/feature": FEAT_REFACTOR_REGEX, + "type/bug": BUG_REGEX, + "type/deprecation": DEPRECATED_REGEX, + "type/enhancement": ENHANCEMENT_REGEX, + }; - // Maintenance: We should keep track of modified PRs in case their titles change - let miss = 0; - try { - for (const label in labels) { - const matcher = new RegExp(labels[label]); - const matches = matcher.exec(PR_TITLE); - if (matches != null) { - core.info(`Auto-labeling PR ${PR_NUMBER} with ${label}`); + // Maintenance: We should keep track of modified PRs in case their titles change + let miss = 0; + try { + for (const label in labels) { + const matcher = new RegExp(labels[label]); + const matches = matcher.exec(PR_TITLE); + if (matches != null) { + core.info(`Auto-labeling PR ${PR_NUMBER} with ${label}`); - await github.rest.issues.addLabels({ - issue_number: PR_NUMBER, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [label] - }); + await github.rest.issues.addLabels({ + issue_number: PR_NUMBER, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [label], + }); - return; - } else { - core.debug(`'${PR_TITLE}' didn't match '${label}' semantic.`); - miss += 1; - } - } - } finally { - if (miss == Object.keys(labels).length) { - core.notice(`PR ${PR_NUMBER} title '${PR_TITLE}' doesn't follow semantic titles; skipping...`); - } + return; + } else { + core.debug(`'${PR_TITLE}' didn't match '${label}' semantic.`); + miss += 1; + } + } + } finally { + if (miss == Object.keys(labels).length) { + core.notice( + `PR ${PR_NUMBER} title '${PR_TITLE}' doesn't follow semantic titles; skipping...` + ); } -} \ No newline at end of file + } +}; diff --git a/.github/scripts/label_related_issue.js b/.github/scripts/label_related_issue.js index 6cfebbdf9b..e57f6621ba 100644 --- a/.github/scripts/label_related_issue.js +++ b/.github/scripts/label_related_issue.js @@ -6,48 +6,86 @@ const { LABEL_PENDING_RELEASE, HANDLE_MAINTAINERS_TEAM, PR_IS_MERGED, + RELATED_ISSUE_REGEX, } = require("./constants"); -module.exports = async ({github, context, core}) => { - if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { - return core.notice("Author in IGNORE_AUTHORS list; skipping..."); - } - - if (PR_IS_MERGED == "false") { - return core.notice("Only merged PRs to avoid spam; skipping"); - } +module.exports = async ({ github, context, core }) => { + if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { + return core.notice("Author in IGNORE_AUTHORS list; skipping..."); + } - const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; - - const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); - - try { - if (!isMatch) { - core.setFailed(`Unable to find related issue for PR number ${PR_NUMBER}.\n\n Body details: ${PR_BODY}`); - return await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - body: `${HANDLE_MAINTAINERS_TEAM} No related issues found. Please ensure '${LABEL_PENDING_RELEASE}' label is applied before releasing.`, - issue_number: PR_NUMBER, - }); - } - } catch (error) { - core.setFailed(`Unable to create comment on PR number ${PR_NUMBER}.\n\n Error details: ${error}`); - throw new Error(error); - } + if (PR_IS_MERGED == "false") { + return core.notice("Only merged PRs to avoid spam; skipping"); + } - const { groups: {issue} } = isMatch; - - try { - core.info(`Auto-labeling related issue ${issue} for release`); - return await github.rest.issues.addLabels({ - issue_number: issue, + const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); + try { + if (!isMatch) { + core.setFailed( + `Unable to find related issue for PR number ${PR_NUMBER}.\n\n Body details: ${PR_BODY}` + ); + return await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, - labels: [LABEL_PENDING_RELEASE], + body: `${HANDLE_MAINTAINERS_TEAM} No related issues found. Please ensure '${LABEL_PENDING_RELEASE}' label is applied before releasing.`, + issue_number: PR_NUMBER, }); - } catch (error) { - core.setFailed(`Is this issue number (${issue}) valid? Perhaps a discussion?`); - throw new Error(error); } -} + } catch (error) { + core.setFailed( + `Unable to create comment on PR number ${PR_NUMBER}.\n\n Error details: ${error}` + ); + throw new Error(error); + } + + const { + groups: { issue }, + } = isMatch; + + let currentLabels = []; + try { + core.info(`Getting labels for issue ${issue}`); + currentLabels = await github.rest.issues.listLabelsOnIssue({ + issue_number: issue, + owner: context.repo.owner, + repo: context.repo.repo, + }); + } catch (error) { + core.setFailed( + `Unable to get labels for issue ${issue}.\n\n Error details: ${error}` + ); + throw new Error(error); + } + + /** + * Keep all labels except those that start with 'status/' or 'need-' or equal to 'help-wanted' + * as those are contextual to issues still in progress. + */ + const newLabels = currentLabels.data + .filter( + (label) => + !label.name.startsWith("status/") && + !label.name.startsWith("need-") && + label.name !== "help-wanted" + ) + .map((label) => label.name); + // Add the status/pending-release label + newLabels.push(LABEL_PENDING_RELEASE); + + try { + core.info( + `Auto-labeling related issue ${issue} for release while removing 'status/*' and 'need-*' labels` + ); + return await github.rest.issues.setLabels({ + issue_number: issue, + owner: context.repo.owner, + repo: context.repo.repo, + labels: newLabels, + }); + } catch (error) { + core.setFailed( + `Is this issue number (${issue}) valid? Perhaps a discussion?` + ); + throw new Error(error); + } +}; diff --git a/.github/scripts/post_release.js b/.github/scripts/post_release.js index 5b6781c475..7eeebeb24b 100644 --- a/.github/scripts/post_release.js +++ b/.github/scripts/post_release.js @@ -11,7 +11,6 @@ const { LABEL_PENDING_RELEASE, LABEL_RELEASED } = require("./constants"); * @return {Object[]} issues - Array of issues matching params * @see {@link https://octokit.github.io/rest.js/v18#usage|Octokit client} */ - const fetchIssues = async ({ gh_client, org, @@ -44,19 +43,19 @@ const fetchIssues = async ({ * @param {string} release_version - GitHub Release version * @see {@link https://octokit.github.io/rest.js/v18#usage|Octokit client} */ - const notifyRelease = async ({ gh_client, owner, repository, release_version, }) => { - const release_url = `https://github.com/${owner}/${repository}/releases/tag/v${release_version}`; + const release_url = `https://github.com/${owner}/${repository}/releases/tag/v${release_version.replace(/v/g, '')}`; const issues = await fetchIssues({ gh_client: gh_client, org: owner, repository: repository, + state: "closed" }); issues.forEach(async (issue) => { @@ -79,15 +78,15 @@ const notifyRelease = async ({ // Close issue and remove staged label; keep existing ones const labels = issue.labels .filter((label) => label.name != LABEL_PENDING_RELEASE) - .map((label) => label.name); + .map((label) => label.name) + .push(LABEL_RELEASED); try { - await gh_client.rest.issues.update({ + await gh_client.rest.issues.setLabels({ repo: repository, - owner: owner, + owner, issue_number: issue.number, - state: "closed", - labels: [...labels, LABEL_RELEASED], + labels, }); } catch (error) { console.error(error); @@ -99,7 +98,6 @@ const notifyRelease = async ({ }; // context: https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts - module.exports = async ({ github, context }) => { const { RELEASE_VERSION } = process.env; console.log(`Running post-release script for ${RELEASE_VERSION} version`); diff --git a/.github/semantic.yml b/.github/semantic.yml index 39119da8d0..e07898ce24 100644 --- a/.github/semantic.yml +++ b/.github/semantic.yml @@ -13,6 +13,23 @@ types: - revert - improv +scopes: + - tracer + - logger + - metrics + - parameters + - idempotency + - commons + - parser + - validator + - batch + - layers + - ci + - docs + - tests + - internal + - maintenance + # Always validate the PR title # and ignore the commits to lower the entry bar for contribution # while titles make up the Release notes to ease maintenance overhead diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml new file mode 100644 index 0000000000..cce883a503 --- /dev/null +++ b/.github/workflows/post-release.yml @@ -0,0 +1,33 @@ +name: Post Release + +on: + # Triggered manually + workflow_dispatch: + inputs: + versionNumber: + required: true + type: string + description: "If running this manually please insert a version number that corresponds to the latest published in the GitHub releases (i.e. v1.1.1)" + # Or triggered as result of a release + release: + types: [released] + +jobs: + post_release: + permissions: + contents: read + issues: write + discussions: write + pull-requests: write + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ inputs.versionNumber }} + steps: + - uses: actions/checkout@v3 + - name: Update issues related to release + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const post_release = require('.github/scripts/post_release.js') + await post_release({github, context, core}) \ No newline at end of file diff --git a/MAINTAINERS.md b/MAINTAINERS.md index d3e514f608..b320bdb9f9 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -69,7 +69,9 @@ These are the most common labels used by maintainers to triage issues, pull requ | area/tracer | Items related to the Tracer Utility | PR automation | | area/idempotency | Items related to the Idempotency Utility | PR automation | | area/parameters | Items related to the Parameters Utility | PR automation | +| area/commons | Items related to the Commons Utility | PR automation | | area/automation | Items related to automation like GitHub workflows or CI/CD | PR automation | +| area/layers | Items related to the Lambda Layers pipeline | PR automation | | size/XS | PRs between 0-9 LOC | PR automation | | size/S | PRs between 10-29 LOC | PR automation | | size/M | PRs between 30-99 LOC | PR automation |