Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: preserve GitHub Actions hashes and versions #2007

Merged
merged 3 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions docs/Configuration Files.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ This includes the options described in [CLI](./CLI.md).
Some of create-typescript-app's options are rich objects, typically very long strings, or otherwise not reasonable on the CLI.
These options are generally only programmatically used internally, but can still be specified in a configuration file:

| Option | Description | Default (If Available) |
| ---------------- | ------------------------------------------------------------------------ | --------------------------------------------------------- |
| `contributors` | AllContributors contributors to store in `.all-contributorsrc` | Existing contributors in the file, or just your username |
| `documentation` | any additional docs to add to `.github/DEVELOPMENT.md` | Extra content in `.github/DEVELOPMENT.md` |
| `existingLabels` | existing labels to switch to the standard template labels | Existing labels on the repository from the GitHub API |
| `explainer` | additional `README.md` sentence(s) describing the package | Extra content in `README.md` after badges and description |
| `guide` | link to a contribution guide to place at the top of development docs | Block quote on top of `.github/DEVELOPMENT.md` |
| `logo` | local image file and alt text to display near the top of the `README.md` | First non-badge image's `alt` and `src` in `README.md` |
| `node` | Node.js engine version(s) to pin and require a minimum of | Values from `.nvmrc` and `package.json`'s `"engines"` |
| `packageData` | additional properties to include in `package.json` | Existing values in `package.json` |
| `rulesetId` | GitHub branch ruleset ID for main branch protections | Existing ruleset on the `main` branch from the GitHub API |
| `usage` | Markdown docs to put in `README.md` under the `## Usage` heading | Existing usage lines in `README.md` |
| Option | Description | Default (If Available) |
| ------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------- |
| `contributors` | AllContributors contributors to store in `.all-contributorsrc` | Existing contributors in the file, or just your username |
| `documentation` | any additional docs to add to `.github/DEVELOPMENT.md` | Extra content in `.github/DEVELOPMENT.md` |
| `existingLabels` | existing labels to switch to the standard template labels | Existing labels on the repository from the GitHub API |
| `explainer` | additional `README.md` sentence(s) describing the package | Extra content in `README.md` after badges and description |
| `guide` | link to a contribution guide to place at the top of development docs | Block quote on top of `.github/DEVELOPMENT.md` |
| `logo` | local image file and alt text to display near the top of the `README.md` | First non-badge image's `alt` and `src` in `README.md` |
| `node` | Node.js engine version(s) to pin and require a minimum of | Values from `.nvmrc` and `package.json`'s `"engines"` |
| `packageData` | additional properties to include in `package.json` | Existing values in `package.json` |
| `rulesetId` | GitHub branch ruleset ID for main branch protections | Existing ruleset on the `main` branch from the GitHub API |
| `usage` | Markdown docs to put in `README.md` under the `## Usage` heading | Existing usage lines in `README.md` |
| `workflowsVersions` | existing versions of GitHub Actions workflows used | Existing action versions in `.github/workflows/*.yml` |

For example, changing `node` versions to values different from what would be inferred:

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"bingo": "^0.5.8",
"bingo-fs": "^0.5.4",
"bingo-stratum": "^0.5.7",
"cached-factory": "^0.1.0",
"cspell-populate-words": "^0.3.0",
"execa": "^9.5.2",
"git-url-parse": "^16.0.1",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe("base", () => {
title: "Create TypeScript App",
usage: expect.any(String),
version: expect.any(String),
workflowsVersions: expect.any(Object),
});
});
});
20 changes: 10 additions & 10 deletions src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,8 @@ import { readRepository } from "./options/readRepository.js";
import { readRulesetId } from "./options/readRulesetId.js";
import { readTitle } from "./options/readTitle.js";
import { readUsage } from "./options/readUsage.js";

const zContributor = z.object({
avatar_url: z.string(),
contributions: z.array(z.string()),
login: z.string(),
name: z.string(),
profile: z.string(),
});

export type Contributor = z.infer<typeof zContributor>;
import { readWorkflowsVersions } from "./options/readWorkflowsVersions.js";
import { zContributor, zWorkflowsVersions } from "./schemas.js";

export const base = createBase({
options: {
Expand Down Expand Up @@ -166,6 +158,9 @@ export const base = createBase({
.string()
.optional()
.describe("package version to publish as and store in `package.json`"),
workflowsVersions: zWorkflowsVersions
.optional()
.describe("existing versions of GitHub Actions workflows used"),
},
prepare({ options, take }) {
const getAccess = lazyValue(async () => await readAccess(getPackageData));
Expand Down Expand Up @@ -278,6 +273,10 @@ export const base = createBase({

const getVersion = lazyValue(async () => (await getPackageData()).version);

const getWorkflowData = lazyValue(
async () => await readWorkflowsVersions(take),
);

return {
access: getAccess,
author: getAuthor,
Expand All @@ -301,6 +300,7 @@ export const base = createBase({
title: getTitle,
usage: getUsage,
version: getVersion,
workflowsVersions: getWorkflowData,
};
},
});
Expand Down
87 changes: 87 additions & 0 deletions src/blocks/actions/resolveUses.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";

import { resolveUses } from "./resolveUses.js";

describe(resolveUses, () => {
it("returns action@version when workflowsVersions is undefined", () => {
const actual = resolveUses("test-action", "v1.2.3");

expect(actual).toBe("[email protected]");
});

it("returns action@version when workflowsVersions does not contain the action", () => {
const actual = resolveUses("test-action", "v1.2.3", { other: {} });

expect(actual).toBe("[email protected]");
});

it("uses the provided version when it is greater than all the action versions in workflowsVersions", () => {
const actual = resolveUses("test-action", "v1.2.3", {
"test-action": {
"v0.1.2": {
pinned: true,
},
"v1.1.4": {
pinned: true,
},
},
});

expect(actual).toBe("[email protected]");
});

it("prefers a provided valid semver version when an action also has a non-semver tag", () => {
const actual = resolveUses("test-action", "v1.2.3", {
"test-action": {
main: {
pinned: true,
},
},
});

expect(actual).toBe("[email protected]");
});

it("prefers an action's semver tag when the provided version is a non-semver tag", () => {
const actual = resolveUses("test-action", "main", {
"test-action": {
"v1.2.3": {
pinned: true,
},
},
});

expect(actual).toBe("[email protected]");
});

it("uses the greatest version when the provided version is not bigger than all the action versions in workflowsVersions", () => {
const actual = resolveUses("test-action", "v1.2.3", {
"test-action": {
"v0.1.2": {
pinned: true,
},
"v1.3.5": {
pinned: true,
},
},
});

expect(actual).toBe("[email protected]");
});

it("uses a pinned hash when the greatest version contains a hash", () => {
const actual = resolveUses("test-action", "v1.2.3", {
"test-action": {
"v0.1.2": {
pinned: true,
},
"v1.3.5": {
hash: "abc",
pinned: true,
},
},
});

expect(actual).toBe("test-action@abc # v1.3.5");
});
});
41 changes: 41 additions & 0 deletions src/blocks/actions/resolveUses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { CachedFactory } from "cached-factory";
import semver from "semver";

import { WorkflowsVersions } from "../../schemas.js";

const semverCoercions = new CachedFactory((version: string) => {
return semver.coerce(version)?.toString() ?? "0.0.0";
});

export function resolveUses(
action: string,
version: string,
workflowsVersions?: WorkflowsVersions,
) {
if (!workflowsVersions || !(action in workflowsVersions)) {
return `${action}@${version}`;
}

const workflowVersions = workflowsVersions[action];

const biggestVersion = Object.keys(workflowVersions).reduce(
(highestVersion, potentialVersion) =>
semver.gt(
semverCoercions.get(potentialVersion),
semverCoercions.get(highestVersion),
)
? potentialVersion
: highestVersion,
version,
);

if (!(biggestVersion in workflowVersions)) {
return `${action}@${biggestVersion}`;
}

const atBiggestVersion = workflowVersions[biggestVersion];

return atBiggestVersion.hash
? `${action}@${atBiggestVersion.hash} # ${biggestVersion}`
: `${action}@${biggestVersion}`;
}
19 changes: 16 additions & 3 deletions src/blocks/blockAllContributors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import _ from "lodash";

import { base, Contributor } from "../base.js";
import { base } from "../base.js";
import { ownerContributions } from "../data/contributions.js";
import { Contributor } from "../schemas.js";
import { resolveUses } from "./actions/resolveUses.js";
import { blockPrettier } from "./blockPrettier.js";
import { blockREADME } from "./blockREADME.js";
import { blockRepositorySecrets } from "./blockRepositorySecrets.js";
Expand Down Expand Up @@ -59,11 +61,22 @@ export const blockAllContributors = base.createBlock({
},
},
steps: [
{ uses: "actions/checkout@v4", with: { "fetch-depth": 0 } },
{
uses: resolveUses(
"actions/checkout",
"v4",
options.workflowsVersions,
),
with: { "fetch-depth": 0 },
},
{ uses: "./.github/actions/prepare" },
{
env: { GITHUB_TOKEN: "${{ secrets.ACCESS_TOKEN }}" },
uses: `JoshuaKGoldberg/[email protected]`,
uses: resolveUses(
"JoshuaKGoldberg/all-contributors-auto-action",
"v0.5.0",
options.workflowsVersions,
),
},
],
}),
Expand Down
15 changes: 12 additions & 3 deletions src/blocks/blockCTATransitions.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { base } from "../base.js";
import { packageData } from "../data/packageData.js";
import { resolveUses } from "./actions/resolveUses.js";
import { blockGitHubActionsCI } from "./blockGitHubActionsCI.js";
import { blockPackageJson } from "./blockPackageJson.js";

export const blockCTATransitions = base.createBlock({
about: {
name: "CTA Transitions",
},
produce() {
produce({ options }) {
return {
addons: [
blockGitHubActionsCI({
Expand All @@ -25,7 +26,11 @@ export const blockCTATransitions = base.createBlock({
steps: [
{ run: "pnpx create-typescript-app" },
{
uses: "stefanzweifel/git-auto-commit-action@v5",
uses: resolveUses(
"stefanzweifel/git-auto-commit-action",
"v5",
options.workflowsVersions,
),
with: {
commit_author: "The Friendly Bingo Bot <[email protected]>",
commit_message:
Expand All @@ -35,7 +40,11 @@ export const blockCTATransitions = base.createBlock({
},
},
{
uses: "mshick/add-pr-comment@v2",
uses: resolveUses(
"mshick/add-pr-comment",
"v2",
options.workflowsVersions,
),
with: {
issue: "${{ github.event.pull_request.number }}",
message: [
Expand Down
9 changes: 7 additions & 2 deletions src/blocks/blockCodecov.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from "zod";

import { base } from "../base.js";
import { resolveUses } from "./actions/resolveUses.js";
import { blockGitHubApps } from "./blockGitHubApps.js";
import { blockRemoveFiles } from "./blockRemoveFiles.js";
import { blockVitest } from "./blockVitest.js";
Expand All @@ -10,7 +11,7 @@ export const blockCodecov = base.createBlock({
name: "Codecov",
},
addons: { env: z.record(z.string(), z.string()).optional() },
produce({ addons }) {
produce({ addons, options }) {
const { env } = addons;
return {
addons: [
Expand All @@ -27,7 +28,11 @@ export const blockCodecov = base.createBlock({
{
...(env && { env }),
if: "always()",
uses: "codecov/codecov-action@v3",
uses: resolveUses(
"codecov/codecov-action",
"v3",
options.workflowsVersions,
),
},
],
}),
Expand Down
Loading