diff --git a/.github/actions/add-label-artifact/action.yaml b/.github/actions/add-label-artifact/action.yaml new file mode 100644 index 000000000000..28c4b294655c --- /dev/null +++ b/.github/actions/add-label-artifact/action.yaml @@ -0,0 +1,27 @@ +name: Add Label Artifact +description: Uploads an empty artifact named `label-${name}=${value}`, that's consumed by action "update-labels" + +inputs: + name: + description: Name + required: true + value: + description: Value ("true" or "false") + required: true + +runs: + using: composite + + steps: + - name: Create empty file to upload artifact + run: "> $RUNNER_TEMP/empty.txt" + shell: bash + + # The maximum length is reported to be 260 characters. A full list of invalid artifact name characters is documented here: + # https://github.com/actions/toolkit/blob/main/packages/artifact/src/internal/upload/path-and-artifact-name-validation.ts + - uses: actions/upload-artifact@v4 + with: + name: label-${{ inputs.name }}=${{ inputs.value }} + path: ${{ runner.temp }}/empty.txt + if-no-files-found: error + overwrite: true diff --git a/.github/actions/context.js b/.github/actions/context.js new file mode 100644 index 000000000000..8dce041c2f22 --- /dev/null +++ b/.github/actions/context.js @@ -0,0 +1,95 @@ +// @ts-check + +/** + * Extracts inputs from context based on event name and properties. + * run_id is only defined for "workflow_run:completed" events. + * + * @param {import('github-script').AsyncFunctionArguments['github']} github + * @param {import('github-script').AsyncFunctionArguments['context']} context + * @param {import('github-script').AsyncFunctionArguments['core']} core + * @returns {Promise<{owner: string, repo: string, head_sha: string, issue_number: number, run_id: number }>} + */ +async function extractInputs(github, context, core) { + core.info(`extractInputs(${context.eventName}, ${context.payload.action})`); + + // Add support for more event types as needed + if (context.eventName === "pull_request") { + const payload = + /** @type {import("@octokit/webhooks-types").PullRequestEvent} */ ( + context.payload + ); + + const inputs = { + owner: payload.repository.owner.login, + repo: payload.repository.name, + head_sha: payload.pull_request.head.sha, + issue_number: payload.number, + run_id: NaN + }; + + core.info(`inputs: ${JSON.stringify(inputs)}`); + + return inputs; + } else if ( + context.eventName === "workflow_run" && + context.payload.action === "completed" + ) { + const payload = + /** @type {import("@octokit/webhooks-types").WorkflowRunCompletedEvent} */ ( + context.payload + ); + + let issue_number; + + const pull_requests = payload.workflow_run.pull_requests; + if (pull_requests && pull_requests.length > 0) { + // For non-fork PRs, we should be able to extract the PR number from the payload, which avoids an + // unnecessary API call. The listPullRequestsAssociatedWithCommit() API also seems to return + // empty for non-fork PRs. + issue_number = pull_requests[0].number; + } else { + // For fork PRs, we must call an API in the head repository to get the PR number in the target repository + + // Owner and repo for the PR head (may differ from target for fork PRs) + const head_owner = payload.workflow_run.head_repository.owner.login; + const head_repo = payload.workflow_run.head_repository.name; + const head_sha = payload.workflow_run.head_sha; + + core.info( + `listPullRequestsAssociatedWithCommit(${head_owner}, ${head_repo}, ${head_sha})`, + ); + const { data: pullRequests } = + await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: head_owner, + repo: head_repo, + commit_sha: head_sha, + }); + + if (pullRequests.length === 1) { + issue_number = pullRequests[0].number; + } else { + throw new Error( + `Unexpected number of pull requests associated with commit '${head_sha}'. Expected: '1'. Actual '${pullRequests.length}'.`, + ); + } + } + + const inputs = { + owner: payload.workflow_run.repository.owner.login, + repo: payload.workflow_run.repository.name, + head_sha: payload.workflow_run.head_sha, + issue_number: issue_number, + run_id: payload.workflow_run.id, + }; + + core.info(`inputs: ${JSON.stringify(inputs)}`); + + return inputs; + } else { + throw new Error( + `Invalid context: '${context.eventName}:${context.payload.action}'. Expected 'workflow_run:completed'.`, + ); + } +} + +module.exports = { extractInputs }; diff --git a/.github/actions/update-labels/action.js b/.github/actions/update-labels/action.js new file mode 100644 index 000000000000..ffd734584b14 --- /dev/null +++ b/.github/actions/update-labels/action.js @@ -0,0 +1,103 @@ +// @ts-check + +const { extractInputs } = require("../context"); + +/** + * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + */ +module.exports = async ({ github, context, core }) => { + let owner = process.env.OWNER; + let repo = process.env.REPO; + let issue_number = parseInt(process.env.ISSUE_NUMBER || ""); + let run_id = parseInt(process.env.RUN_ID || ""); + + if (!owner || !repo || !(issue_number || run_id)) { + let inputs = await extractInputs(github, context, core); + owner = owner || inputs.owner; + repo = repo || inputs.repo; + issue_number = issue_number || inputs.issue_number; + run_id = run_id || inputs.run_id; + } + + /** @type {string[]} */ + let artifactNames = []; + + if (run_id) { + // List artifacts from a single run_id + core.info(`listWorkflowRunArtifacts(${owner}, ${repo}, ${run_id})`); + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: owner, + repo: repo, + run_id: run_id, + }); + + artifactNames = artifacts.data.artifacts.map((a) => a.name); + } else { + // TODO: List all artifacts of all workflows associated with issue_number + throw new Error("Required input 'run_id' not found in env or context"); + } + + core.info(`artifactNames: ${JSON.stringify(artifactNames)}`); + + /** @type {string[]} */ + const labelsToAdd = []; + + /** @type {string[]} */ + const labelsToRemove = []; + + for (const artifactName of artifactNames) { + // If artifactName has format "label-name=true|false", add or remove the label + // Else, if artifactName has format "label-name=other-string", throw an error + // Else, if artifactName does not start with "label-", ignore it + const firstEquals = artifactName.indexOf("="); + if (firstEquals !== -1) { + const key = artifactName.substring(0, firstEquals); + const value = artifactName.substring(firstEquals + 1); + + if (key.startsWith("label-")) { + const name = key.substring("label-".length); + if (value === "true") { + labelsToAdd.push(name); + } else if (value === "false") { + labelsToRemove.push(name); + } else { + throw new Error( + `Invalid value for label '${name}': ${value}. Expected "true" or "false".`, + ); + } + } + } + } + + core.info(`labelsToAdd: ${JSON.stringify(labelsToAdd)}`); + core.info(`labelsToRemove: ${JSON.stringify(labelsToRemove)}`); + + if (labelsToAdd.length > 0) { + await github.rest.issues.addLabels({ + owner: owner, + repo: repo, + issue_number: issue_number, + labels: labelsToAdd, + }); + } + + if (labelsToRemove.length > 0) { + // Must loop over labelsToRemove ourselves, since GitHub doesn't expose a REST API to remove in bulk. + for (const name of labelsToRemove) { + try { + await github.rest.issues.removeLabel({ + owner: owner, + repo: repo, + issue_number: issue_number, + name: name, + }); + } catch (error) { + if (error.status === 404) { + core.info(`Ignoring error: ${error.status} - ${error.message}`); + } else { + throw error; + } + } + } + } +}; diff --git a/.github/actions/update-labels/action.yaml b/.github/actions/update-labels/action.yaml new file mode 100644 index 000000000000..d87902dcba48 --- /dev/null +++ b/.github/actions/update-labels/action.yaml @@ -0,0 +1,33 @@ +name: Update Labels +description: Adds or removes labels to set state matching artifact names + +# If any inputs are not set, we will attempt to extract them from the event context +inputs: + owner: + description: The account owner of the repository. The name is not case sensitive. + required: false + repo: + description: The name of the repository without the .git extension. The name is not case sensitive. + required: false + issue_number: + description: The issue that should have its labels updated. + required: false + run_id: + description: Updates labels from a single completed workflow. + required: false + +runs: + using: composite + + steps: + - name: Set Label + uses: actions/github-script@v7 + env: + OWNER: ${{ inputs.owner }} + REPO: ${{ inputs.repo }} + ISSUE_NUMBER: ${{ inputs.issue_number }} + RUN_ID: ${{ inputs.run_id }} + with: + script: | + const action = require('./.github/actions/update-labels/action.js') + await action({ github, context, core }); diff --git a/.github/package-lock.json b/.github/package-lock.json new file mode 100644 index 000000000000..9bf44423ccfa --- /dev/null +++ b/.github/package-lock.json @@ -0,0 +1,475 @@ +{ + "name": "scripts", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@octokit/webhooks-types": "^7.5.1", + "@types/github-script": "github:actions/github-script", + "@types/node": "^20.0.0", + "prettier": "^3.3.3" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", + "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0" + } + }, + "node_modules/@actions/glob": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.4.0.tgz", + "integrity": "sha512-+eKIGFhsFa4EBwaf/GMyzCdWrXWymGXfFmZU3FHQvYS8mPcHtTtZONbkcqqUMzw9mJ/pImEBFET1JNifhqGsAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/core": "^1.9.1", + "minimatch": "^3.0.4" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", + "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", + "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@octokit/webhooks-types": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz", + "integrity": "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/github-script": { + "name": "github-script", + "version": "7.0.1", + "resolved": "git+ssh://git@github.com/actions/github-script.git#660ec11d825b714d112a6bb9727086bc2cc500b2", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/exec": "^1.1.1", + "@actions/github": "^6.0.0", + "@actions/glob": "^0.4.0", + "@actions/io": "^1.1.3", + "@octokit/core": "^5.0.1", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-retry": "^6.0.1", + "@types/node": "^20.9.0" + }, + "engines": { + "node": ">=20.0.0 <21.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.17.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.3.tgz", + "integrity": "sha512-tSQrmKKatLDGnG92h40GD7FzUt0MjahaHwOME4VAFeeA/Xopayq5qLyQRy7Jg/pjgKIFBXuKcGhJo+UdYG55jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "Apache-2.0" + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "ISC" + } + } +} diff --git a/.github/package.json b/.github/package.json new file mode 100644 index 000000000000..87598b90e726 --- /dev/null +++ b/.github/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "@types/node": "^20.0.0", + "@types/github-script": "github:actions/github-script", + "@octokit/webhooks-types": "^7.5.1", + "prettier": "^3.3.3" + } +} diff --git a/.github/workflows/typespec-requirement.yaml b/.github/workflows/typespec-requirement.yaml index 3f3b4c9831b6..47909f268cfd 100644 --- a/.github/workflows/typespec-requirement.yaml +++ b/.github/workflows/typespec-requirement.yaml @@ -5,25 +5,29 @@ on: pull_request jobs: TypeSpec-Requirement: name: TypeSpec Requirement - strategy: - fail-fast: false - matrix: - spec-type: [data-plane, resource-manager] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - # Required since "HEAD^" is passed to TypeSpec-Requirement.ps1 - fetch-depth: 2 - - - name: Setup Node and run `npm ci` - uses: ./.github/actions/setup-node-npm-ci - - - run: | - eng/scripts/TypeSpec-Requirement.ps1 ` - -BaseCommitish HEAD^ ` - -TargetCommitish HEAD ` - -SpecType ${{ matrix.spec-type }} - shell: pwsh + - uses: actions/checkout@v4 + with: + # Required since "HEAD^" is passed to TypeSpec-Requirement.ps1 + fetch-depth: 2 + + - name: Setup Node and run `npm ci` + uses: ./.github/actions/setup-node-npm-ci + + - run: | + eng/scripts/TypeSpec-Requirement.ps1 ` + -BaseCommitish HEAD^ ` + -TargetCommitish HEAD ` + id: tsr-ps1 + shell: pwsh + + # Always add label artifact, even if "brownfield=false", to ensure label is removed when necessary + - uses: ./.github/actions/add-label-artifact + name: Upload artifact with results + with: + name: "brownfield" + value: "${{ steps.tsr-ps1.outputs.brownfield == 'true' }}" + diff --git a/.github/workflows/update-labels.yaml b/.github/workflows/update-labels.yaml new file mode 100644 index 000000000000..d1663f57ae8c --- /dev/null +++ b/.github/workflows/update-labels.yaml @@ -0,0 +1,54 @@ +name: Update Labels + +on: + # TODO: If a pull request is manually labeled or unlabeled, get all artifacts associated with PR, and update labels + # pull_request: + # types: [labeled, unlabeled] + # If an upstream workflow if completed, get only the artifacts from that workflow, and update labels + workflow_run: + workflows: ["TypeSpec Requirement"] + types: [completed] + workflow_dispatch: + inputs: + owner: + description: The account owner of the repository. The name is not case sensitive. + required: true + type: string + repo: + description: The name of the repository without the .git extension. The name is not case sensitive. + required: true + type: string + # simulate pull_request trigger + issue_number: + description: The number that identifies the issue. + required: false + type: number + # simulate workflow_run trigger + run_id: + description: The unique identifier of the workflow run. + required: false + type: number + +jobs: + update-labels: + name: Update Labels + + permissions: + contents: read + pull-requests: write + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + .github + + - name: Update Labels + uses: ./.github/actions/update-labels + with: + owner: ${{ inputs.owner }} + repo: ${{ inputs.repo }} + issue_number: ${{ inputs.issue_number }} + run_id: ${{ inputs.run_id }} diff --git a/.gitignore b/.gitignore index 937f93a4aa54..b07c3b3ae14e 100644 --- a/.gitignore +++ b/.gitignore @@ -118,6 +118,7 @@ warnings.txt # Blanket ignores *.js +!.github/**/*.js *.d.ts *.js.map *.d.ts.map @@ -131,9 +132,10 @@ eng/tools/**/dist # TypeScript cache *.tsbuildinfo -# No package-lock.json files should be commited except the top-level. +# No package-lock.json files should be commited except the top-level and .github **/package-lock.json !/package-lock.json +!/.github/package-lock.json # No Armstrong outputs should be commited except the tf files. **/terraform/**/*.json diff --git a/eng/scripts/TypeSpec-Requirement.ps1 b/eng/scripts/TypeSpec-Requirement.ps1 index d1ea69b4914a..6e0e0eb74971 100644 --- a/eng/scripts/TypeSpec-Requirement.ps1 +++ b/eng/scripts/TypeSpec-Requirement.ps1 @@ -168,6 +168,14 @@ else { if ($responseStatus -eq 200) { LogInfo " Branch 'main' contains path '$servicePath/stable', so spec already exists and is not required to use TypeSpec" + + $notice = "Brownfield services will soon be required to convert from OpenAPI to TypeSpec. See https://aka.ms/azsdk/typespec." + LogNoticeForFile $file $notice + + if ($env:GITHUB_OUTPUT) { + # Set output to be used later in /.github/workflows/TypeSpec-Requirement.yaml + Add-Content -Path $env:GITHUB_OUTPUT -Value "brownfield=true" + } } elseif ($responseStatus -eq 404) { LogInfo " Branch 'main' does not contain path '$servicePath/stable', so spec is new and must use TypeSpec" diff --git a/eng/tools/typespec-requirement/test/typespec-requirement.test.ts b/eng/tools/typespec-requirement/test/typespec-requirement.test.ts index fa01dffde1e1..e4872dcef5c9 100644 --- a/eng/tools/typespec-requirement/test/typespec-requirement.test.ts +++ b/eng/tools/typespec-requirement/test/typespec-requirement.test.ts @@ -57,6 +57,8 @@ test.concurrent("Hand-written, exists in main", async ({ expect }) => { expect(stdout).toContain("was not generated from TypeSpec"); expect(stdout).toContain("'main' contains path"); + expect(stdout.toLowerCase()).toContain("notice"); + expect(stdout).toContain("will soon be required to convert"); expect(exitCode).toBe(0); });