Skip to content

Commit d48f6ca

Browse files
committed
docs: Documenting support for full lock files
1 parent fdd2ec2 commit d48f6ca

4 files changed

Lines changed: 260 additions & 1 deletion

File tree

docs/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"start": "astro dev",
88
"build": "astro build",
99
"preview": "astro preview",
10-
"astro": "astro"
10+
"astro": "astro",
11+
"test": "bun test"
1112
},
1213
"dependencies": {
1314
"@astrojs/compiler-rs": "^0.1.6",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
version: "v1.0.4"
3+
category: "new-features"
4+
---
5+
6+
import { Aside } from '@astrojs/starlight/components'
7+
8+
#### Full `.terraform.lock.hcl` files from the provider cache server
9+
10+
When the [provider cache server](/features/caching/provider-cache-server) is used against the OpenTofu provider registry, Terragrunt now writes `.terraform.lock.hcl` files containing `h1:` hashes for every platform the registry supports. A single `terragrunt init` produces a lock file that works on every platform, removing the need to run `tofu providers lock -platform=...` separately for each target architecture.
11+
12+
```hcl
13+
provider "registry.opentofu.org/hashicorp/null" {
14+
version = "3.2.2"
15+
constraints = "3.2.2"
16+
hashes = [
17+
"h1:+1mRmfyz6oA00IhrrSkHK3h/Mdh032x2p0F6OMdMo5s=",
18+
"h1:FjLTqvaaYo+vHN8pHZB1cOwEGiNzOj+I9kQyHmr9/7o=",
19+
# ... one entry per supported platform ...
20+
"zh:00e5877d19fb1c1d8c4b3536334a46a5c86f57146fd115c7b7b4b5d2bf2de86d",
21+
# ... one entry per supported platform ...
22+
]
23+
}
24+
```
25+
26+
The hashes come from the registry's per-platform download response. When the registry does not supply them (for example, a third-party registry that has not adopted the field), Terragrunt falls back to its previous behavior of writing an `h1:` hash for the current platform plus `zh:` hashes for every platform listed in the shasums document.
27+
28+
<Aside type="tip" title="Thanks to the OpenTofu team">
29+
This feature builds on work done by the OpenTofu team to expose per-platform hashes directly from the OpenTofu provider registry. Starting with [OpenTofu 1.12](https://opentofu.org/), `tofu init` populates `.terraform.lock.hcl` with hashes for every supported platform out of the box, with no Terragrunt or `tofu providers lock` invocation required. Users on older OpenTofu versions still get the same lock files when running through Terragrunt's provider cache server, but upgrading to 1.12 is the easiest way to get the same behavior everywhere.
30+
</Aside>

docs/src/lib/changelog.test.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { describe, expect, test } from "bun:test";
2+
import {
3+
categorySlugSort,
4+
compareVersionsDesc,
5+
isReleased,
6+
parsePullRequests,
7+
prepareForGitHub,
8+
pullRequestsToMarkdown,
9+
} from "./changelog";
10+
11+
const SITE = "https://terragrunt.gruntwork.io";
12+
13+
describe("compareVersionsDesc", () => {
14+
test("orders semver versions descending", () => {
15+
const sorted = ["v1.0.10", "v1.0.2", "v1.0.0"].sort(compareVersionsDesc);
16+
expect(sorted).toEqual(["v1.0.10", "v1.0.2", "v1.0.0"]);
17+
});
18+
19+
test("non-version strings sort before semver versions", () => {
20+
const sorted = ["v1.0.0", "draft", "v0.99.0"].sort(compareVersionsDesc);
21+
expect(sorted[0]).toBe("draft");
22+
});
23+
});
24+
25+
describe("isReleased", () => {
26+
test("returns true when version is at or below latest", () => {
27+
expect(isReleased("v1.0.3", "1.0.3")).toBe(true);
28+
expect(isReleased("v1.0.0", "1.0.3")).toBe(true);
29+
});
30+
31+
test("returns false when version is newer than latest", () => {
32+
expect(isReleased("v1.0.4", "1.0.3")).toBe(false);
33+
});
34+
35+
test("returns false for non-semver tags", () => {
36+
expect(isReleased("draft", "1.0.3")).toBe(false);
37+
});
38+
});
39+
40+
describe("categorySlugSort", () => {
41+
test("uses the canonical category order", () => {
42+
const slugs = ["bug-fixes", "breaking-changes", "new-features"].sort(categorySlugSort);
43+
expect(slugs).toEqual(["breaking-changes", "new-features", "bug-fixes"]);
44+
});
45+
});
46+
47+
describe("parsePullRequests", () => {
48+
test("returns empty array on null body", () => {
49+
expect(parsePullRequests(null)).toEqual([]);
50+
});
51+
52+
test("groups items under their conventional type and stops at next h2", () => {
53+
const body = [
54+
"## What's Changed",
55+
"* feat: add a thing by @alice in https://github.com/o/r/pull/1",
56+
"* fix(parser): tighten regex by @bob in https://github.com/o/r/pull/2",
57+
"* feat!: rip out the old API by @carol in https://github.com/o/r/pull/3",
58+
"* docs: tidy README by @dan in https://github.com/o/r/pull/4",
59+
"",
60+
"## New Contributors",
61+
"* should not be parsed by @eve in https://github.com/o/r/pull/99",
62+
].join("\n");
63+
64+
const groups = parsePullRequests(body);
65+
const labels = groups.map((g) => g.type.key);
66+
67+
expect(labels).toEqual(["breaking", "feat", "fix", "docs"]);
68+
expect(groups[1].items[0].prNumber).toBe(1);
69+
expect(groups[0].items[0].prNumber).toBe(3);
70+
expect(groups.flatMap((g) => g.items.map((i) => i.author))).not.toContain("eve");
71+
});
72+
73+
test("ignores text outside the What's Changed section", () => {
74+
const body = [
75+
"Intro paragraph",
76+
"* fix: should not be picked up by @x in https://github.com/o/r/pull/1",
77+
"## What's Changed",
78+
"* feat: real entry by @y in https://github.com/o/r/pull/2",
79+
].join("\n");
80+
81+
const groups = parsePullRequests(body);
82+
expect(groups).toHaveLength(1);
83+
expect(groups[0].items[0].author).toBe("y");
84+
});
85+
});
86+
87+
describe("pullRequestsToMarkdown", () => {
88+
test("formats grouped items with author and PR links", () => {
89+
const md = pullRequestsToMarkdown(
90+
parsePullRequests(
91+
[
92+
"## What's Changed",
93+
"* feat: add a thing by @alice in https://github.com/o/r/pull/1",
94+
].join("\n"),
95+
),
96+
);
97+
98+
expect(md).toContain("## Pull Requests");
99+
expect(md).toContain("### ✨ Features");
100+
expect(md).toContain("[@alice](https://github.com/alice)");
101+
expect(md).toContain("[#1](https://github.com/o/r/pull/1)");
102+
});
103+
});
104+
105+
describe("prepareForGitHub", () => {
106+
test("rewrites root-relative links to absolute URLs", () => {
107+
const out = prepareForGitHub("See [the docs](/features/x).", SITE);
108+
expect(out).toBe(`See [the docs](${SITE}/features/x).`);
109+
});
110+
111+
test("flattens entry and category headings to h2 and strips horizontal rules", () => {
112+
// The wrapping page renders categories as `### Label` and entries as
113+
// `#### Title`. The two-step rewrite below collapses both to `##`, since
114+
// pass two also catches the `###` produced by pass one. GitHub releases
115+
// render `##` as the largest distinguishable section heading.
116+
const out = prepareForGitHub(
117+
["### ✨ New Features", "", "---", "", "#### Did a thing", "", "Body."].join("\n"),
118+
SITE,
119+
);
120+
expect(out).toBe(["## ✨ New Features", "", "## Did a thing", "", "Body."].join("\n"));
121+
});
122+
123+
test("strips MDX import lines", () => {
124+
const out = prepareForGitHub(
125+
[
126+
"import { Aside } from '@astrojs/starlight/components'",
127+
"",
128+
"#### Title",
129+
"",
130+
"Body.",
131+
].join("\n"),
132+
SITE,
133+
);
134+
expect(out).toBe(["## Title", "", "Body."].join("\n"));
135+
});
136+
137+
test("converts a tip Aside with a title to a GitHub TIP alert", () => {
138+
const out = prepareForGitHub(
139+
['<Aside type="tip" title="Heads up">', "Be aware.", "</Aside>"].join("\n"),
140+
SITE,
141+
);
142+
expect(out).toBe(["> [!TIP]", "> **Heads up**", ">", "> Be aware."].join("\n"));
143+
});
144+
145+
test("uses NOTE for default Aside type and omits title line when absent", () => {
146+
const out = prepareForGitHub("<Aside>Just a note.</Aside>", SITE);
147+
expect(out).toBe(["> [!NOTE]", "> Just a note."].join("\n"));
148+
});
149+
150+
test("maps Starlight caution and danger to WARNING and CAUTION", () => {
151+
const caution = prepareForGitHub('<Aside type="caution">be careful</Aside>', SITE);
152+
const danger = prepareForGitHub('<Aside type="danger">stop</Aside>', SITE);
153+
expect(caution).toContain("> [!WARNING]");
154+
expect(danger).toContain("> [!CAUTION]");
155+
});
156+
157+
test("transforms multiple Asides independently", () => {
158+
const out = prepareForGitHub(
159+
['<Aside type="tip">first</Aside>', "", '<Aside type="caution">second</Aside>'].join("\n"),
160+
SITE,
161+
);
162+
expect(out).toContain("> [!TIP]");
163+
expect(out).toContain("> first");
164+
expect(out).toContain("> [!WARNING]");
165+
expect(out).toContain("> second");
166+
});
167+
168+
test("preserves blank lines inside an Aside body as `>` continuation rows", () => {
169+
const out = prepareForGitHub(
170+
[
171+
'<Aside type="note" title="Heads up">',
172+
"para one",
173+
"",
174+
"para two",
175+
"</Aside>",
176+
].join("\n"),
177+
SITE,
178+
);
179+
expect(out).toContain("> para one");
180+
expect(out).toContain(">\n> para two");
181+
});
182+
183+
test("collapses runs of blank lines to a single blank line", () => {
184+
const out = prepareForGitHub("a\n\n\n\nb", SITE);
185+
expect(out).toBe("a\n\nb");
186+
});
187+
});

docs/src/lib/changelog.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,50 @@ export function pullRequestsToMarkdown(groups: PullRequestGroup[]): string {
218218
return `## Pull Requests\n\n${sections.join("\n\n")}`;
219219
}
220220

221+
// Maps Starlight `<Aside type="...">` values to the closest GitHub Flavored
222+
// Markdown alert label. GitHub renders these as colored callout boxes in
223+
// release notes and READMEs.
224+
const ASIDE_TYPE_TO_GH_ALERT: Record<string, string> = {
225+
note: "NOTE",
226+
tip: "TIP",
227+
caution: "WARNING",
228+
danger: "CAUTION",
229+
};
230+
231+
const ASIDE_BLOCK = /<Aside(?:\s+([^>]*?))?\s*>([\s\S]*?)<\/Aside>/g;
232+
const ASIDE_TYPE_ATTR = /\btype\s*=\s*"([^"]+)"/;
233+
const ASIDE_TITLE_ATTR = /\btitle\s*=\s*"([^"]+)"/;
234+
const MDX_IMPORT_LINE = /^\s*import\s+[^\n]*\s+from\s+['"][^'"]+['"];?\s*$/gm;
235+
236+
function attrValue(attrs: string, pattern: RegExp): string | null {
237+
const match = attrs.match(pattern);
238+
return match ? match[1] : null;
239+
}
240+
241+
function transformAsidesToGitHubAlerts(input: string): string {
242+
return input.replace(ASIDE_BLOCK, (_match, attrs: string | undefined, content: string) => {
243+
const attrString = attrs ?? "";
244+
const type = attrValue(attrString, ASIDE_TYPE_ATTR) ?? "note";
245+
const title = attrValue(attrString, ASIDE_TITLE_ATTR) ?? "";
246+
const alert = ASIDE_TYPE_TO_GH_ALERT[type] ?? "NOTE";
247+
248+
const titleSection = title ? `**${title}**\n\n` : "";
249+
const body = `[!${alert}]\n${titleSection}${content.trim()}`;
250+
251+
return body
252+
.split("\n")
253+
.map((line) => (line.length === 0 ? ">" : `> ${line}`))
254+
.join("\n");
255+
});
256+
}
257+
221258
export function prepareForGitHub(body: string, siteUrl: string): string {
222259
let result = body;
223260

261+
result = result.replace(MDX_IMPORT_LINE, "");
262+
263+
result = transformAsidesToGitHubAlerts(result);
264+
224265
result = result.replace(/\]\(\//g, `](${siteUrl}/`);
225266

226267
result = result.replace(/^####\s/gm, "### ");

0 commit comments

Comments
 (0)