Skip to content

Commit

Permalink
ci: easy pre-releases for new packages (#4757)
Browse files Browse the repository at this point in the history
* ci: easy pre-releases for new packages

* fixup! ci: easy pre-releases for new packages

Expose ACTIONS_RUNTIME_TOKEN and ACTIONS_RESULTS_URL variables

* fixup! ci: easy pre-releases for new packages

Switch to `workers-sdk` object in `package.json`

* fixup! ci: easy pre-releases for new packages

Remove extraneous `|` in pre-release comment

* fixup! ci: easy pre-releases for new packages

Simplify validation/extraction of `githubPullRequestNumber`

* fixup! ci: easy pre-releases for new packages

Use `forEach` instead of `for of`

* fixup! ci: easy pre-releases for new packages

Explain why we need custom action for extracting action variables

* fixup! fixup! ci: easy pre-releases for new packages
  • Loading branch information
mrbbot authored Jan 19, 2024
1 parent d467626 commit f860aba
Show file tree
Hide file tree
Showing 15 changed files with 973 additions and 92 deletions.
5 changes: 5 additions & 0 deletions .github/actions/expose-actions-variables/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: "Expose Actions Variables"
description: "Expose ACTIONS_RUNTIME_TOKEN and ACTIONS_RESULTS_URL on GITHUB_ENV"
runs:
using: "node20"
main: "index.mjs"
7 changes: 7 additions & 0 deletions .github/actions/expose-actions-variables/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import fs from "node:fs";

fs.appendFileSync(
process.env.GITHUB_ENV,
`GITHUB_ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}\n` +
`GITHUB_ACTIONS_RESULTS_URL=${process.env.ACTIONS_RESULTS_URL}\n`
);
1 change: 1 addition & 0 deletions .github/extract-runtime-versions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const workerdBinary = path.resolve(workerdPackageJsonPath, "../bin/workerd");

const workerdBinaryVersion = execSync(workerdBinary + " --version")
.toString()
.trim()
.split(" ")[1];

// 4. Write basic markdown report
Expand Down
101 changes: 101 additions & 0 deletions .github/prereleases/0-packages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import assert from "node:assert";
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const projectRoot = path.resolve(__dirname, "../..");

const githubRunId = parseInt(process.env.GITHUB_RUN_ID);
assert(
!Number.isNaN(githubRunId),
"Expected GITHUB_RUN_ID variable to be a number"
);
const githubEventPath = process.env.GITHUB_EVENT_PATH;
assert(githubEventPath, "Expected GITHUB_EVENT_PATH variable to be defined");
const githubEventContents = fs.readFileSync(githubEventPath, "utf8");
const githubEvent = JSON.parse(githubEventContents);
const githubPullRequestNumber = githubEvent?.pull_request?.number;
assert(
typeof githubPullRequestNumber === "number",
`Expected valid pull_request event, got ${githubEventContents}`
);

/**
* @typedef {object} ~PackageJsonWorkersSdk
* @property {boolean} [prerelease]
*/

/**
* @typedef {object} ~PackageJson
* @property {string} name
* @property {string} version
* @property {Record<string, string>} [dependencies]
* @property {Record<string, string>} [devDependencies]
* @property {Record<string, string>} [peerDependencies]
* @property {Record<string, string>} [optionalDependencies]
* @property {~PackageJsonWorkersSdk} [workers-sdk]
*/

/**
* @typedef {object} ~Package
* @property {string} path
* @property {~PackageJson} json
*/

/** @returns {string[]} */
function getPackagePaths() {
const stdout = execSync(
'pnpm list --filter="./packages/*" --recursive --depth=-1 --parseable',
{ cwd: projectRoot, encoding: "utf8" }
);
return stdout.split("\n").filter((pkgPath) => path.isAbsolute(pkgPath));
}

/**
* @param {string} pkgPath
* @returns {~Package}
*/
function getPackage(pkgPath) {
const json = fs.readFileSync(path.join(pkgPath, "package.json"), "utf8");
return {
path: pkgPath,
json: JSON.parse(json),
};
}

/** @param {~Package} pkg */
export function setPackage(pkg) {
const json = JSON.stringify(pkg.json, null, "\t");
fs.writeFileSync(path.join(pkg.path, "package.json"), json);
}

/** @returns {~Package[]} */
function getPackages() {
return getPackagePaths().map(getPackage);
}

/** @returns {~Package[]} */
export function getPackagesForPrerelease() {
return getPackages().filter((pkg) => pkg.json["workers-sdk"]?.prerelease);
}

/** @param {string} pkgName */
export function getPrereleaseArtifactName(pkgName) {
const name = pkgName.replaceAll("@", "").replaceAll("/", "-");
return `npm-package-${name}-${githubPullRequestNumber}`;
}

/** @param {string} pkgName */
export function getPrereleaseArtifactUrl(pkgName) {
const artifactName = getPrereleaseArtifactName(pkgName);
return `https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/${githubRunId}/${artifactName}`;
}

/** @param {string} pkgName */
export function getPrereleasePRArtifactUrl(pkgName) {
const artifactName = getPrereleaseArtifactName(pkgName);
return `https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/prs/${githubPullRequestNumber}/${artifactName}`;
}
43 changes: 43 additions & 0 deletions .github/prereleases/1-versions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { execSync } from "node:child_process";
import {
getPackagesForPrerelease,
getPrereleaseArtifactUrl,
setPackage,
} from "./0-packages.mjs";

function getPrereleaseVersion() {
const sha = execSync("git rev-parse --short HEAD", { encoding: "utf8" });
return `0.0.0-${sha.trim()}`;
}

/**
* @param {~Package[]} pkgs
* @param {string} newVersion
*/
function updateVersions(pkgs, newVersion) {
for (const pkg of pkgs) pkg.json.version = newVersion;
}

/**
* @param {~Package[]} pkgs
* @param {string} newVersion
*/
function updateDependencyVersions(pkgs, newVersion) {
const prereleaseNames = new Set(pkgs.map((pkg) => pkg.json.name));
for (const pkg of pkgs) {
for (const dependency of Object.keys(pkg.json.dependencies ?? {})) {
if (prereleaseNames.has(dependency)) {
pkg.json.dependencies[dependency] =
getPrereleaseArtifactUrl(dependency);
}
}
}
}

{
const pkgs = getPackagesForPrerelease();
const newVersion = getPrereleaseVersion();
updateVersions(pkgs, newVersion);
updateDependencyVersions(pkgs, newVersion);
pkgs.forEach(setPackage);
}
45 changes: 45 additions & 0 deletions .github/prereleases/2-build-pack-upload.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import assert from "node:assert";
import { execSync } from "node:child_process";
import path from "node:path";
import { DefaultArtifactClient } from "@actions/artifact";
import {
getPackagesForPrerelease,
getPrereleaseArtifactName,
projectRoot,
} from "./0-packages.mjs";

const artifact = new DefaultArtifactClient();

function buildAllPackages() {
execSync("pnpm build", { cwd: projectRoot, stdio: "inherit" });
}

/**
* @param {~Package} pkg
* @returns {string}
*/
function packPackage(pkg) {
const stdout = execSync("pnpm pack", { cwd: pkg.path, encoding: "utf8" });
const name = stdout.split("\n").find((line) => line.endsWith(".tgz"));
assert(name !== undefined, `Expected ${stdout} to include tarball name`);
return path.join(pkg.path, name);
}

/**
* @param {~Package} pkg
* @param {string} tarballPath
*/
async function uploadPackageTarball(pkg, tarballPath) {
const name = getPrereleaseArtifactName(pkg.json.name);
console.log(`Uploading ${tarballPath} as ${name}...`);
await artifact.uploadArtifact(name, [tarballPath], pkg.path);
}

{
buildAllPackages();
const pkgs = getPackagesForPrerelease();
for (const pkg of pkgs) {
const tarballPath = packPackage(pkg);
await uploadPackageTarball(pkg, tarballPath);
}
}
90 changes: 90 additions & 0 deletions .github/prereleases/3-comment.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import assert from "node:assert";
import fs from "node:fs";
import path from "node:path";
import { DefaultArtifactClient } from "@actions/artifact";
import {
getPackagesForPrerelease,
getPrereleaseArtifactUrl,
getPrereleasePRArtifactUrl,
projectRoot,
} from "./0-packages.mjs";

const artifact = new DefaultArtifactClient();

/**
* @param {~Package} pkg
* @returns {string}
*/
function buildWranglerArtifactReport(pkg) {
const name = pkg.json.name;
assert.strictEqual(name, "wrangler");
const url = getPrereleaseArtifactUrl(name);
const prUrl = getPrereleasePRArtifactUrl(name);
return `A wrangler prerelease is available for testing. You can install this latest build in your project with:
\`\`\`sh
npm install --save-dev ${url}
\`\`\`
You can reference the automatically updated head of this PR with:
\`\`\`sh
npm install --save-dev ${prUrl}
\`\`\`
Or you can use \`npx\` with this latest build directly:
\`\`\`sh
npx ${url} dev path/to/script.js
\`\`\``;
}

/**
* @param {~Package} pkg
* @returns {string}
*/
function buildAdditionalArtifactReport(pkg) {
const name = pkg.json.name;
const url = getPrereleaseArtifactUrl(name);
if (name === "create-cloudflare") {
return `\`\`\`sh\nnpx ${url} --no-auto-update\n\`\`\``;
} else {
return `\`\`\`sh\nnpm install ${url}\n\`\`\``;
}
}

/**
* @param {~Package[]} pkgs
* @returns {string}
*/
function buildReport(pkgs) {
const wranglerPkgIndex = pkgs.findIndex(
(pkg) => pkg.json.name === "wrangler"
);
assert(wranglerPkgIndex !== -1, "Expected wrangler to be pre-released");
const [wranglerPkg] = pkgs.splice(wranglerPkgIndex, 1);

const wranglerReport = buildWranglerArtifactReport(wranglerPkg);
const additionalReports = pkgs.map(buildAdditionalArtifactReport);

return `${wranglerReport}
<details><summary>Additional artifacts:</summary>
${additionalReports.join("\n\n")}
Note that these links will no longer work once [the GitHub Actions artifact expires](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization).
</details>
`;
}

{
const pkgs = getPackagesForPrerelease();
const report = buildReport(pkgs);
const reportName = "prerelease-report.md";
const reportPath = path.join(projectRoot, reportName);
fs.writeFileSync(reportPath, report);
console.log(`Uploading ${reportPath} as ${reportName}...`);
await artifact.uploadArtifact(reportName, [reportPath], projectRoot);
}
80 changes: 28 additions & 52 deletions .github/workflows/create-pullrequest-prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,62 +51,38 @@ jobs:
name: runtime-versions.md
path: runtime-versions.md

- name: Modify miniflare package.json version
run: node .github/version-script.js miniflare

- name: Modify wrangler package.json version
run: node .github/version-script.js

- name: Modify wrangler package.json miniflare dependency
run: cat <<< $(jq --tab ".dependencies.miniflare = \"$MINIFLARE_VERSION\"" packages/wrangler/package.json) > packages/wrangler/package.json
env:
MINIFLARE_VERSION: https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/${{ github.run_id }}/npm-package-miniflare-${{ github.event.number }}

- name: Build
run: pnpm run build
- name: Expose ACTIONS_RUNTIME_TOKEN and ACTIONS_RESULTS_URL
# We use the `@actions/artifact` package in the prerelease scripts below.
# This is meant to be used from custom JavaScript actions, rather than
# workflow `run` steps. This means it expects `ACTIONS_RUNTIME_TOKEN`
# and `ACTIONS_RESULTS_URL` environment variables to be set. We'd like
# to retain separation between the different stages of pre-releasing,
# and easily pass environment variables to the build. Therefore, we use
# this stub action that exposes the `ACTIONS_RUNTIME_TOKEN` and
# `ACTIONS_RESULTS_URL` action-specific environment variables as
# `GITHUB_ACTIONS_RUNTIME_TOKEN` and `GITHUB_ACTIONS_RESULTS_URL` global
# environment variables. These can be passed via `env` to `run` steps
# that use `@actions/artifact`.
uses: ./.github/actions/expose-actions-variables

- name: Update versions and dependencies
run: node .github/prereleases/1-versions.mjs

- name: Build, pack, and upload packages
run: node .github/prereleases/2-build-pack-upload.mjs
env:
NODE_ENV: "production"
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }}
SENTRY_DSN: "https://9edbb8417b284aa2bbead9b4c318918b@sentry10.cfdata.org/583"
CI_OS: ${{ runner.os }}
# Required by the `@actions/artifact` package
ACTIONS_RUNTIME_TOKEN: ${{ env.GITHUB_ACTIONS_RUNTIME_TOKEN }}
ACTIONS_RESULTS_URL: ${{ env.GITHUB_ACTIONS_RESULTS_URL }}

- name: Pack miniflare
run: pnpm pack
working-directory: packages/miniflare

- name: Upload packaged miniflare artifact
uses: actions/upload-artifact@v3
with:
name: npm-package-miniflare-${{ github.event.number }} # encode the PR number into the artifact name
path: packages/miniflare/miniflare-*.tgz

- name: Pack wrangler
run: pnpm pack
working-directory: packages/wrangler

- name: Upload packaged wrangler artifact
uses: actions/upload-artifact@v3
with:
name: npm-package-wrangler-${{ github.event.number }} # encode the PR number into the artifact name
path: packages/wrangler/wrangler-*.tgz

- name: Pack @cloudflare/pages-shared
run: pnpm pack
working-directory: packages/pages-shared

- name: Upload packaged @cloudflare/pages-shared artifact
uses: actions/upload-artifact@v3
with:
name: npm-package-cloudflare-pages-shared-${{ github.event.number }} # encode the PR number into the artifact name
path: packages/pages-shared/cloudflare-pages-shared-*.tgz

- name: Pack @cloudflare/create-cloudflare
run: pnpm pack
working-directory: packages/create-cloudflare

- name: Upload packaged @cloudflare/create-cloudflare artifact
uses: actions/upload-artifact@v3
with:
name: npm-package-create-cloudflare-${{ github.event.number }} # encode the PR number into the artifact name
path: packages/create-cloudflare/create-cloudflare-*.tgz
- name: Build and upload prerelease comment
run: node .github/prereleases/3-comment.mjs
env:
# Required by the `@actions/artifact` package
ACTIONS_RUNTIME_TOKEN: ${{ env.GITHUB_ACTIONS_RUNTIME_TOKEN }}
ACTIONS_RESULTS_URL: ${{ env.GITHUB_ACTIONS_RESULTS_URL }}
Loading

0 comments on commit f860aba

Please sign in to comment.