Skip to content

Commit 56b2826

Browse files
fix: preserve GitHub Actions hashes and versions (#2007)
## PR Checklist - [x] Addresses an existing open issue: fixes #1998 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md) were taken ## Overview Adds a new `workflowsVersions` option that reads in any existing pins and comment hashes. Printing out comment hashes is actually tricky: they're not part of the AST, so I had to add a post-processing step after YAMl printing to turn any `uses:` string ending with a `#` comment back from `uses: '... # ...'` to `uses: ... # ...`. Skips adding tests for `readWorkflowVersions` pending bingo-js/bingo#308. 🎁
1 parent aa860fd commit 56b2826

20 files changed

+422
-63
lines changed

docs/Configuration Files.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,19 @@ This includes the options described in [CLI](./CLI.md).
1919
Some of create-typescript-app's options are rich objects, typically very long strings, or otherwise not reasonable on the CLI.
2020
These options are generally only programmatically used internally, but can still be specified in a configuration file:
2121

22-
| Option | Description | Default (If Available) |
23-
| ---------------- | ------------------------------------------------------------------------ | --------------------------------------------------------- |
24-
| `contributors` | AllContributors contributors to store in `.all-contributorsrc` | Existing contributors in the file, or just your username |
25-
| `documentation` | any additional docs to add to `.github/DEVELOPMENT.md` | Extra content in `.github/DEVELOPMENT.md` |
26-
| `existingLabels` | existing labels to switch to the standard template labels | Existing labels on the repository from the GitHub API |
27-
| `explainer` | additional `README.md` sentence(s) describing the package | Extra content in `README.md` after badges and description |
28-
| `guide` | link to a contribution guide to place at the top of development docs | Block quote on top of `.github/DEVELOPMENT.md` |
29-
| `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` |
30-
| `node` | Node.js engine version(s) to pin and require a minimum of | Values from `.nvmrc` and `package.json`'s `"engines"` |
31-
| `packageData` | additional properties to include in `package.json` | Existing values in `package.json` |
32-
| `rulesetId` | GitHub branch ruleset ID for main branch protections | Existing ruleset on the `main` branch from the GitHub API |
33-
| `usage` | Markdown docs to put in `README.md` under the `## Usage` heading | Existing usage lines in `README.md` |
22+
| Option | Description | Default (If Available) |
23+
| ------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------- |
24+
| `contributors` | AllContributors contributors to store in `.all-contributorsrc` | Existing contributors in the file, or just your username |
25+
| `documentation` | any additional docs to add to `.github/DEVELOPMENT.md` | Extra content in `.github/DEVELOPMENT.md` |
26+
| `existingLabels` | existing labels to switch to the standard template labels | Existing labels on the repository from the GitHub API |
27+
| `explainer` | additional `README.md` sentence(s) describing the package | Extra content in `README.md` after badges and description |
28+
| `guide` | link to a contribution guide to place at the top of development docs | Block quote on top of `.github/DEVELOPMENT.md` |
29+
| `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` |
30+
| `node` | Node.js engine version(s) to pin and require a minimum of | Values from `.nvmrc` and `package.json`'s `"engines"` |
31+
| `packageData` | additional properties to include in `package.json` | Existing values in `package.json` |
32+
| `rulesetId` | GitHub branch ruleset ID for main branch protections | Existing ruleset on the `main` branch from the GitHub API |
33+
| `usage` | Markdown docs to put in `README.md` under the `## Usage` heading | Existing usage lines in `README.md` |
34+
| `workflowsVersions` | existing versions of GitHub Actions workflows used | Existing action versions in `.github/workflows/*.yml` |
3435

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"bingo": "^0.5.8",
4141
"bingo-fs": "^0.5.4",
4242
"bingo-stratum": "^0.5.7",
43+
"cached-factory": "^0.1.0",
4344
"cspell-populate-words": "^0.3.0",
4445
"execa": "^9.5.2",
4546
"git-url-parse": "^16.0.1",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/base.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ describe("base", () => {
5353
title: "Create TypeScript App",
5454
usage: expect.any(String),
5555
version: expect.any(String),
56+
workflowsVersions: expect.any(Object),
5657
});
5758
});
5859
});

src/base.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,8 @@ import { readRepository } from "./options/readRepository.js";
3131
import { readRulesetId } from "./options/readRulesetId.js";
3232
import { readTitle } from "./options/readTitle.js";
3333
import { readUsage } from "./options/readUsage.js";
34-
35-
const zContributor = z.object({
36-
avatar_url: z.string(),
37-
contributions: z.array(z.string()),
38-
login: z.string(),
39-
name: z.string(),
40-
profile: z.string(),
41-
});
42-
43-
export type Contributor = z.infer<typeof zContributor>;
34+
import { readWorkflowsVersions } from "./options/readWorkflowsVersions.js";
35+
import { zContributor, zWorkflowsVersions } from "./schemas.js";
4436

4537
export const base = createBase({
4638
options: {
@@ -166,6 +158,9 @@ export const base = createBase({
166158
.string()
167159
.optional()
168160
.describe("package version to publish as and store in `package.json`"),
161+
workflowsVersions: zWorkflowsVersions
162+
.optional()
163+
.describe("existing versions of GitHub Actions workflows used"),
169164
},
170165
prepare({ options, take }) {
171166
const getAccess = lazyValue(async () => await readAccess(getPackageData));
@@ -278,6 +273,10 @@ export const base = createBase({
278273

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

276+
const getWorkflowData = lazyValue(
277+
async () => await readWorkflowsVersions(take),
278+
);
279+
281280
return {
282281
access: getAccess,
283282
author: getAuthor,
@@ -301,6 +300,7 @@ export const base = createBase({
301300
title: getTitle,
302301
usage: getUsage,
303302
version: getVersion,
303+
workflowsVersions: getWorkflowData,
304304
};
305305
},
306306
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { resolveUses } from "./resolveUses.js";
4+
5+
describe(resolveUses, () => {
6+
it("returns action@version when workflowsVersions is undefined", () => {
7+
const actual = resolveUses("test-action", "v1.2.3");
8+
9+
expect(actual).toBe("[email protected]");
10+
});
11+
12+
it("returns action@version when workflowsVersions does not contain the action", () => {
13+
const actual = resolveUses("test-action", "v1.2.3", { other: {} });
14+
15+
expect(actual).toBe("[email protected]");
16+
});
17+
18+
it("uses the provided version when it is greater than all the action versions in workflowsVersions", () => {
19+
const actual = resolveUses("test-action", "v1.2.3", {
20+
"test-action": {
21+
"v0.1.2": {
22+
pinned: true,
23+
},
24+
"v1.1.4": {
25+
pinned: true,
26+
},
27+
},
28+
});
29+
30+
expect(actual).toBe("[email protected]");
31+
});
32+
33+
it("prefers a provided valid semver version when an action also has a non-semver tag", () => {
34+
const actual = resolveUses("test-action", "v1.2.3", {
35+
"test-action": {
36+
main: {
37+
pinned: true,
38+
},
39+
},
40+
});
41+
42+
expect(actual).toBe("[email protected]");
43+
});
44+
45+
it("prefers an action's semver tag when the provided version is a non-semver tag", () => {
46+
const actual = resolveUses("test-action", "main", {
47+
"test-action": {
48+
"v1.2.3": {
49+
pinned: true,
50+
},
51+
},
52+
});
53+
54+
expect(actual).toBe("[email protected]");
55+
});
56+
57+
it("uses the greatest version when the provided version is not bigger than all the action versions in workflowsVersions", () => {
58+
const actual = resolveUses("test-action", "v1.2.3", {
59+
"test-action": {
60+
"v0.1.2": {
61+
pinned: true,
62+
},
63+
"v1.3.5": {
64+
pinned: true,
65+
},
66+
},
67+
});
68+
69+
expect(actual).toBe("[email protected]");
70+
});
71+
72+
it("uses a pinned hash when the greatest version contains a hash", () => {
73+
const actual = resolveUses("test-action", "v1.2.3", {
74+
"test-action": {
75+
"v0.1.2": {
76+
pinned: true,
77+
},
78+
"v1.3.5": {
79+
hash: "abc",
80+
pinned: true,
81+
},
82+
},
83+
});
84+
85+
expect(actual).toBe("test-action@abc # v1.3.5");
86+
});
87+
});

src/blocks/actions/resolveUses.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { CachedFactory } from "cached-factory";
2+
import semver from "semver";
3+
4+
import { WorkflowsVersions } from "../../schemas.js";
5+
6+
const semverCoercions = new CachedFactory((version: string) => {
7+
return semver.coerce(version)?.toString() ?? "0.0.0";
8+
});
9+
10+
export function resolveUses(
11+
action: string,
12+
version: string,
13+
workflowsVersions?: WorkflowsVersions,
14+
) {
15+
if (!workflowsVersions || !(action in workflowsVersions)) {
16+
return `${action}@${version}`;
17+
}
18+
19+
const workflowVersions = workflowsVersions[action];
20+
21+
const biggestVersion = Object.keys(workflowVersions).reduce(
22+
(highestVersion, potentialVersion) =>
23+
semver.gt(
24+
semverCoercions.get(potentialVersion),
25+
semverCoercions.get(highestVersion),
26+
)
27+
? potentialVersion
28+
: highestVersion,
29+
version,
30+
);
31+
32+
if (!(biggestVersion in workflowVersions)) {
33+
return `${action}@${biggestVersion}`;
34+
}
35+
36+
const atBiggestVersion = workflowVersions[biggestVersion];
37+
38+
return atBiggestVersion.hash
39+
? `${action}@${atBiggestVersion.hash} # ${biggestVersion}`
40+
: `${action}@${biggestVersion}`;
41+
}

src/blocks/blockAllContributors.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import _ from "lodash";
22

3-
import { base, Contributor } from "../base.js";
3+
import { base } from "../base.js";
44
import { ownerContributions } from "../data/contributions.js";
5+
import { Contributor } from "../schemas.js";
6+
import { resolveUses } from "./actions/resolveUses.js";
57
import { blockPrettier } from "./blockPrettier.js";
68
import { blockREADME } from "./blockREADME.js";
79
import { blockRepositorySecrets } from "./blockRepositorySecrets.js";
@@ -59,11 +61,22 @@ export const blockAllContributors = base.createBlock({
5961
},
6062
},
6163
steps: [
62-
{ uses: "actions/checkout@v4", with: { "fetch-depth": 0 } },
64+
{
65+
uses: resolveUses(
66+
"actions/checkout",
67+
"v4",
68+
options.workflowsVersions,
69+
),
70+
with: { "fetch-depth": 0 },
71+
},
6372
{ uses: "./.github/actions/prepare" },
6473
{
6574
env: { GITHUB_TOKEN: "${{ secrets.ACCESS_TOKEN }}" },
66-
uses: `JoshuaKGoldberg/[email protected]`,
75+
uses: resolveUses(
76+
"JoshuaKGoldberg/all-contributors-auto-action",
77+
"v0.5.0",
78+
options.workflowsVersions,
79+
),
6780
},
6881
],
6982
}),

src/blocks/blockCTATransitions.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { base } from "../base.js";
22
import { packageData } from "../data/packageData.js";
3+
import { resolveUses } from "./actions/resolveUses.js";
34
import { blockGitHubActionsCI } from "./blockGitHubActionsCI.js";
45
import { blockPackageJson } from "./blockPackageJson.js";
56

67
export const blockCTATransitions = base.createBlock({
78
about: {
89
name: "CTA Transitions",
910
},
10-
produce() {
11+
produce({ options }) {
1112
return {
1213
addons: [
1314
blockGitHubActionsCI({
@@ -25,7 +26,11 @@ export const blockCTATransitions = base.createBlock({
2526
steps: [
2627
{ run: "pnpx create-typescript-app" },
2728
{
28-
uses: "stefanzweifel/git-auto-commit-action@v5",
29+
uses: resolveUses(
30+
"stefanzweifel/git-auto-commit-action",
31+
"v5",
32+
options.workflowsVersions,
33+
),
2934
with: {
3035
commit_author: "The Friendly Bingo Bot <[email protected]>",
3136
commit_message:
@@ -35,7 +40,11 @@ export const blockCTATransitions = base.createBlock({
3540
},
3641
},
3742
{
38-
uses: "mshick/add-pr-comment@v2",
43+
uses: resolveUses(
44+
"mshick/add-pr-comment",
45+
"v2",
46+
options.workflowsVersions,
47+
),
3948
with: {
4049
issue: "${{ github.event.pull_request.number }}",
4150
message: [

src/blocks/blockCodecov.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from "zod";
22

33
import { base } from "../base.js";
4+
import { resolveUses } from "./actions/resolveUses.js";
45
import { blockGitHubApps } from "./blockGitHubApps.js";
56
import { blockRemoveFiles } from "./blockRemoveFiles.js";
67
import { blockVitest } from "./blockVitest.js";
@@ -10,7 +11,7 @@ export const blockCodecov = base.createBlock({
1011
name: "Codecov",
1112
},
1213
addons: { env: z.record(z.string(), z.string()).optional() },
13-
produce({ addons }) {
14+
produce({ addons, options }) {
1415
const { env } = addons;
1516
return {
1617
addons: [
@@ -27,7 +28,11 @@ export const blockCodecov = base.createBlock({
2728
{
2829
...(env && { env }),
2930
if: "always()",
30-
uses: "codecov/codecov-action@v3",
31+
uses: resolveUses(
32+
"codecov/codecov-action",
33+
"v3",
34+
options.workflowsVersions,
35+
),
3136
},
3237
],
3338
}),

0 commit comments

Comments
 (0)