setup-git-repo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/bin/setup-git-repo.js +289 -0
- package/package.json +48 -0
- package/src/git.mjs +74 -0
- package/src/resolve-template.mjs +44 -0
- package/src/template.mjs +142 -0
- package/template/.github/scripts/next-free-version.mjs +73 -0
- package/template/.github/workflows/auto-merge-and-create-prs.yml +96 -0
- package/template/.github/workflows/auto-merge-claude.yml +50 -0
- package/template/.github/workflows/deploy-test-reports.yml +85 -0
- package/template/.github/workflows/npm-publish.yml +394 -0
- package/template/.github/workflows/tests.yml +116 -0
- package/template/README.md +89 -0
- package/template/apps/test-reports/package.json +14 -0
- package/template/apps/test-reports/wrangler.jsonc +10 -0
- package/template/codecov.yml +50 -0
- package/template/docs/BADGES.md +271 -0
- package/template/docs/SECRETS.md +77 -0
- package/template/docs/WORKFLOWS.md +205 -0
- package/template/gitignore +11 -0
- package/template/package.json +35 -0
- package/template/packages/README.md +1 -0
- package/template/turbo.json +42 -0
- package/template/vitest.config.ts +54 -0
package/src/template.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every optional badge in the template README carries a `<!-- badge:NAME -->`
|
|
6
|
+
* marker at the start of its line. A badge whose value the user did not supply
|
|
7
|
+
* would render as a broken image pointing at a literal `{{DOI}}`, so its whole
|
|
8
|
+
* line is dropped; a badge that is kept has only its marker removed.
|
|
9
|
+
*
|
|
10
|
+
* Maps marker name -> the placeholders that badge needs to be renderable.
|
|
11
|
+
*/
|
|
12
|
+
export const OPTIONAL_BADGES = {
|
|
13
|
+
doi: ["DOI"],
|
|
14
|
+
docs: ["DOCS_URL"],
|
|
15
|
+
api: ["API_URL"],
|
|
16
|
+
youtube: ["YOUTUBE_URL"],
|
|
17
|
+
npm: ["PACKAGE"],
|
|
18
|
+
uptime: ["UPTIME_ID"],
|
|
19
|
+
discord: ["DISCORD_ID", "DISCORD_INVITE"],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const BADGE_MARKER = /<!--\s*badge:([a-z0-9-]+)\s*-->/i;
|
|
23
|
+
|
|
24
|
+
/** Badge names whose placeholders all have a non-empty value. */
|
|
25
|
+
export function configuredBadges(values) {
|
|
26
|
+
return new Set(
|
|
27
|
+
Object.entries(OPTIONAL_BADGES)
|
|
28
|
+
.filter(([, keys]) => keys.every((k) => typeof values[k] === "string" && values[k].trim() !== ""))
|
|
29
|
+
.map(([name]) => name),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Drop the lines of unconfigured badges and strip markers from the rest.
|
|
35
|
+
*
|
|
36
|
+
* Operates line by line, which is why the template keeps one `<a>` per line.
|
|
37
|
+
*/
|
|
38
|
+
export function applyBadges(content, values) {
|
|
39
|
+
const keep = configuredBadges(values);
|
|
40
|
+
const out = [];
|
|
41
|
+
|
|
42
|
+
for (const line of content.split("\n")) {
|
|
43
|
+
const match = line.match(BADGE_MARKER);
|
|
44
|
+
if (!match) {
|
|
45
|
+
out.push(line);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!keep.has(match[1].toLowerCase())) continue;
|
|
49
|
+
// Normalize indentation: the marker sits at column 0, but the badge
|
|
50
|
+
// lines around it are indented, and the block is read by humans.
|
|
51
|
+
out.push(` ${line.replace(BADGE_MARKER, "").trimStart()}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return out.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Replace every `{{KEY}}` with its value.
|
|
59
|
+
*
|
|
60
|
+
* An unknown or empty key is left as-is rather than replaced with an empty
|
|
61
|
+
* string: a visible `{{THING}}` in the output is a bug you notice, whereas a
|
|
62
|
+
* silent blank in a URL is one you ship.
|
|
63
|
+
*/
|
|
64
|
+
export function substitute(content, values) {
|
|
65
|
+
return content.replace(/\{\{([A-Z0-9_]+)\}\}/g, (whole, key) => {
|
|
66
|
+
const value = values[key];
|
|
67
|
+
return typeof value === "string" && value.trim() !== "" ? value : whole;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Render one template file: badge pruning first, then placeholders. */
|
|
72
|
+
export function render(content, values) {
|
|
73
|
+
return substitute(applyBadges(content, values), values);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Recursively list files under `dir`, as paths relative to it, sorted. */
|
|
77
|
+
export function collectFiles(dir) {
|
|
78
|
+
const out = [];
|
|
79
|
+
|
|
80
|
+
const walk = (current) => {
|
|
81
|
+
for (const entry of readdirSync(current).sort()) {
|
|
82
|
+
const full = join(current, entry);
|
|
83
|
+
if (statSync(full).isDirectory()) {
|
|
84
|
+
// Nothing in the template needs these, and copying a stale build into a
|
|
85
|
+
// fresh repo is worse than useless.
|
|
86
|
+
if (entry === "node_modules" || entry === ".turbo" || entry === "dist") continue;
|
|
87
|
+
walk(full);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
out.push(relative(dir, full).split(sep).join("/"));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
walk(dir);
|
|
95
|
+
return out.sort();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Decide what to do with each template file given what already exists.
|
|
100
|
+
*
|
|
101
|
+
* `only` narrows the run to a subset — the `--workflows-only` / `--badges-only`
|
|
102
|
+
* flags — matched as path prefixes.
|
|
103
|
+
*/
|
|
104
|
+
export function planFiles(files, { exists, force = false, only = null }) {
|
|
105
|
+
const plan = { write: [], overwrite: [], skip: [] };
|
|
106
|
+
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
if (only && !only.some((prefix) => file === prefix || file.startsWith(`${prefix}/`))) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!exists(file)) {
|
|
112
|
+
plan.write.push(file);
|
|
113
|
+
} else if (force) {
|
|
114
|
+
plan.overwrite.push(file);
|
|
115
|
+
} else {
|
|
116
|
+
plan.skip.push(file);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return plan;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Where a template file lands in the target repo.
|
|
125
|
+
*
|
|
126
|
+
* `gitignore` becomes `.gitignore`. It is stored without the dot because npm
|
|
127
|
+
* strips `.gitignore` out of published tarballs — a template that carried one
|
|
128
|
+
* would work from a checkout and silently lose the file when installed from the
|
|
129
|
+
* registry.
|
|
130
|
+
*/
|
|
131
|
+
export function destinationFor(file) {
|
|
132
|
+
if (file === "gitignore") return ".gitignore";
|
|
133
|
+
return file;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Path prefixes selected by the `--*-only` flags. */
|
|
137
|
+
export const SUBSETS = {
|
|
138
|
+
workflows: [".github"],
|
|
139
|
+
badges: ["README.md"],
|
|
140
|
+
docs: ["docs"],
|
|
141
|
+
turbo: ["turbo.json", "package.json", "vitest.config.ts"],
|
|
142
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Print the next version of a package that the registry has not already spent.
|
|
4
|
+
*
|
|
5
|
+
* Bumping one patch above the `latest` dist-tag is not enough. `latest` lags
|
|
6
|
+
* any version that an interrupted publish staged, and npm answers a PUT for one
|
|
7
|
+
* of those with `E409 ... Cannot publish over previously staged version`. Those
|
|
8
|
+
* numbers show up in the full version list and in the release timeline, so both
|
|
9
|
+
* are consulted here.
|
|
10
|
+
*
|
|
11
|
+
* node .github/scripts/next-free-version.mjs <package-name> <current-version>
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
const [, , name, current] = process.argv;
|
|
16
|
+
|
|
17
|
+
if (!name || !current) {
|
|
18
|
+
console.error("usage: next-free-version.mjs <package-name> <current-version>");
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Run `npm view` and parse JSON, treating any failure as "no data". */
|
|
23
|
+
function npmView(field) {
|
|
24
|
+
try {
|
|
25
|
+
const out = execFileSync("npm", ["view", name, field, "--json"], {
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
28
|
+
});
|
|
29
|
+
return JSON.parse(out);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const taken = new Set();
|
|
36
|
+
|
|
37
|
+
const versions = npmView("versions");
|
|
38
|
+
if (Array.isArray(versions)) for (const v of versions) taken.add(v);
|
|
39
|
+
else if (typeof versions === "string") taken.add(versions);
|
|
40
|
+
|
|
41
|
+
// `time` carries every version the registry has a timestamp for, including ones
|
|
42
|
+
// that were unpublished or staged and never completed.
|
|
43
|
+
const time = npmView("time");
|
|
44
|
+
if (time && typeof time === "object") {
|
|
45
|
+
for (const key of Object.keys(time)) {
|
|
46
|
+
if (key !== "created" && key !== "modified") taken.add(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parse = (v) => {
|
|
51
|
+
const [core] = String(v).split(/[-+]/);
|
|
52
|
+
const parts = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
53
|
+
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Start from the highest number the registry knows about, not from `current`:
|
|
57
|
+
// a local version behind the registry would otherwise propose versions that are
|
|
58
|
+
// all taken, one refused publish at a time.
|
|
59
|
+
let [major, minor, patch] = parse(current);
|
|
60
|
+
for (const v of taken) {
|
|
61
|
+
const [ma, mi, pa] = parse(v);
|
|
62
|
+
if (ma > major || (ma === major && (mi > minor || (mi === minor && pa > patch)))) {
|
|
63
|
+
[major, minor, patch] = [ma, mi, pa];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let next;
|
|
68
|
+
do {
|
|
69
|
+
patch += 1;
|
|
70
|
+
next = `${major}.${minor}.${patch}`;
|
|
71
|
+
} while (taken.has(next));
|
|
72
|
+
|
|
73
|
+
console.log(next);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# A twice-daily sweep that keeps branches from going stale:
|
|
2
|
+
# 1. merges open PRs that are already clean, approved (or unreviewed) and green
|
|
3
|
+
# 2. opens a PR for any pushed branch that never got one
|
|
4
|
+
#
|
|
5
|
+
# Both halves are idempotent, so a delayed or duplicated run is harmless.
|
|
6
|
+
name: Auto-merge PRs and create missing PRs
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
schedule:
|
|
10
|
+
# 00:00 and 12:00 UTC. Scheduled workflows can be delayed for hours during
|
|
11
|
+
# periods of high GitHub Actions load — never rely on the exact minute.
|
|
12
|
+
- cron: "0 */12 * * *"
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
pull-requests: write
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: auto-merge-and-create-prs
|
|
21
|
+
cancel-in-progress: false
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
maintain-pull-requests:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
env:
|
|
27
|
+
GH_TOKEN: ${{ secrets.GIT_TOKEN }}
|
|
28
|
+
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
|
29
|
+
BASE_BRANCH: ${{ github.event.repository.default_branch }}
|
|
30
|
+
steps:
|
|
31
|
+
- name: Merge eligible existing pull requests
|
|
32
|
+
env:
|
|
33
|
+
REPOSITORY: ${{ github.repository }}
|
|
34
|
+
run: |
|
|
35
|
+
set -euo pipefail
|
|
36
|
+
|
|
37
|
+
# Eligible means: not a draft, mergeable with no conflicts (CLEAN),
|
|
38
|
+
# either approved or not reviewed at all, and every check that
|
|
39
|
+
# reported came back SUCCESS/SKIPPED/NEUTRAL.
|
|
40
|
+
gh pr list \
|
|
41
|
+
--repo "$REPOSITORY" \
|
|
42
|
+
--state open \
|
|
43
|
+
--json number,isDraft,mergeStateStatus,reviewDecision,statusCheckRollup \
|
|
44
|
+
--jq '.[] | select(.isDraft == false) | select(.mergeStateStatus == "CLEAN") | select((.reviewDecision == "APPROVED") or (.reviewDecision == "")) | select(all(.statusCheckRollup[]?; (.conclusion == "SUCCESS" or .conclusion == "SKIPPED" or .conclusion == "NEUTRAL"))) | .number' \
|
|
45
|
+
| while read -r pr_number; do
|
|
46
|
+
echo "Attempting to merge PR #${pr_number}"
|
|
47
|
+
gh pr merge "$pr_number" \
|
|
48
|
+
--repo "$REPOSITORY" \
|
|
49
|
+
--merge \
|
|
50
|
+
--delete-branch \
|
|
51
|
+
--auto || echo "Could not merge PR #${pr_number}; it may require review, checks, or conflict resolution."
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
- name: Create pull requests for branches without one
|
|
55
|
+
env:
|
|
56
|
+
REPOSITORY: ${{ github.repository }}
|
|
57
|
+
run: |
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
|
|
60
|
+
# No actions/checkout above: this step only needs refs, so it fetches
|
|
61
|
+
# them into an empty workspace instead of paying for a full checkout.
|
|
62
|
+
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
63
|
+
git init -q "$GITHUB_WORKSPACE"
|
|
64
|
+
cd "$GITHUB_WORKSPACE"
|
|
65
|
+
git remote add origin "https://x-access-token:${GIT_TOKEN}@github.com/${REPOSITORY}.git"
|
|
66
|
+
git fetch --quiet origin '+refs/heads/*:refs/remotes/origin/*'
|
|
67
|
+
|
|
68
|
+
base_sha="$(git rev-parse "origin/${BASE_BRANCH}")"
|
|
69
|
+
|
|
70
|
+
git for-each-ref --format='%(refname:strip=3)' refs/remotes/origin \
|
|
71
|
+
| grep -Ev "^(HEAD|${BASE_BRANCH})$" \
|
|
72
|
+
| while read -r branch; do
|
|
73
|
+
open_pr="$(gh pr list --repo "$REPOSITORY" --state open --head "$branch" --json number --jq 'length')"
|
|
74
|
+
merged_pr="$(gh pr list --repo "$REPOSITORY" --state merged --head "$branch" --json number --jq 'length')"
|
|
75
|
+
branch_sha="$(git rev-parse "origin/${branch}")"
|
|
76
|
+
|
|
77
|
+
# A merged PR counts: reopening one for a branch whose work
|
|
78
|
+
# already landed would create an empty, permanently open PR.
|
|
79
|
+
if [ "$open_pr" -gt 0 ] || [ "$merged_pr" -gt 0 ]; then
|
|
80
|
+
echo "Skipping ${branch}: a pull request already exists or was previously merged."
|
|
81
|
+
continue
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if git merge-base --is-ancestor "$branch_sha" "$base_sha"; then
|
|
85
|
+
echo "Skipping ${branch}: it is already contained in ${BASE_BRANCH}."
|
|
86
|
+
continue
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
echo "Creating PR for ${branch} -> ${BASE_BRANCH}"
|
|
90
|
+
gh pr create \
|
|
91
|
+
--repo "$REPOSITORY" \
|
|
92
|
+
--head "$branch" \
|
|
93
|
+
--base "$BASE_BRANCH" \
|
|
94
|
+
--title "Merge ${branch} into ${BASE_BRANCH}" \
|
|
95
|
+
--body "Automated pull request for branch \`${branch}\`."
|
|
96
|
+
done
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Enables auto-merge on pull requests opened by trusted agents and maintainers,
|
|
2
|
+
# so an agent-authored change lands on its own once required checks pass.
|
|
3
|
+
#
|
|
4
|
+
# `--auto` is the safe path: GitHub holds the merge until branch protection is
|
|
5
|
+
# satisfied. It only works when "Allow auto-merge" is on in repo settings AND
|
|
6
|
+
# the base branch has protection with at least one required status check —
|
|
7
|
+
# without both, GitHub rejects the flag and the fallback below merges directly.
|
|
8
|
+
# On a repo with no branch protection, that fallback merges without waiting for
|
|
9
|
+
# CI, which is why the actor allowlist matters.
|
|
10
|
+
name: Auto-merge agent PRs
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
pull_request:
|
|
14
|
+
types: [opened, synchronize, reopened]
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
auto-merge:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
# Extend this list with the bots and maintainers whose PRs may self-merge.
|
|
24
|
+
if: >
|
|
25
|
+
github.actor == 'claude[bot]' ||
|
|
26
|
+
github.actor == 'anthropic-claude[bot]' ||
|
|
27
|
+
github.actor == '{{OWNER}}'
|
|
28
|
+
steps:
|
|
29
|
+
- name: Enable auto-merge (or merge directly)
|
|
30
|
+
env:
|
|
31
|
+
# A PAT rather than GITHUB_TOKEN: merges made with GITHUB_TOKEN do not
|
|
32
|
+
# trigger further workflows, so deploy/publish runs on the default
|
|
33
|
+
# branch would never fire.
|
|
34
|
+
GH_TOKEN: ${{ secrets.GIT_TOKEN }}
|
|
35
|
+
PR: ${{ github.event.pull_request.number }}
|
|
36
|
+
REPO: ${{ github.repository }}
|
|
37
|
+
HEAD_REF: ${{ github.head_ref }}
|
|
38
|
+
run: |
|
|
39
|
+
# Long-lived branches must survive their own merges.
|
|
40
|
+
case "$HEAD_REF" in
|
|
41
|
+
production|prod|staging|develop) MERGE_OPTS="--squash" ;;
|
|
42
|
+
*) MERGE_OPTS="--squash --delete-branch" ;;
|
|
43
|
+
esac
|
|
44
|
+
|
|
45
|
+
if gh pr merge "$PR" --repo "$REPO" $MERGE_OPTS --auto; then
|
|
46
|
+
echo "Auto-merge enabled for PR #$PR"
|
|
47
|
+
else
|
|
48
|
+
echo "Auto-merge unavailable, attempting direct merge"
|
|
49
|
+
gh pr merge "$PR" --repo "$REPO" $MERGE_OPTS
|
|
50
|
+
fi
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Publishes a browsable HTML test report to Cloudflare Workers on every push to
|
|
2
|
+
# the default branch, so the README's "Test Report" badge links somewhere real.
|
|
3
|
+
name: Test Reports
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [{{DEFAULT_BRANCH}}]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
deploy:
|
|
12
|
+
name: Generate & Deploy Test Reports
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: oven-sh/setup-bun@v2
|
|
18
|
+
|
|
19
|
+
- name: Install dependencies
|
|
20
|
+
run: bun install --ignore-scripts
|
|
21
|
+
|
|
22
|
+
- name: Build workspace libraries consumed via dist
|
|
23
|
+
run: bunx turbo run build
|
|
24
|
+
|
|
25
|
+
- name: Run tests with HTML reporter
|
|
26
|
+
run: bun run test:report
|
|
27
|
+
# A red suite must still publish its report — that report is how you
|
|
28
|
+
# find out what went red.
|
|
29
|
+
continue-on-error: true
|
|
30
|
+
|
|
31
|
+
# What the step above must not do is leave `dist` missing: `wrangler
|
|
32
|
+
# deploy` treats an absent `assets.directory` as a hard error, so the run
|
|
33
|
+
# would end on a config error instead of on the test signal. Publish a
|
|
34
|
+
# placeholder rather than lose the deploy.
|
|
35
|
+
- name: Ensure a report exists to deploy
|
|
36
|
+
env:
|
|
37
|
+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
38
|
+
run: |
|
|
39
|
+
if [ -f apps/test-reports/dist/index.html ]; then
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
echo "::warning::Vitest wrote no HTML report; deploying a placeholder page."
|
|
43
|
+
mkdir -p apps/test-reports/dist
|
|
44
|
+
cat > apps/test-reports/dist/index.html <<HTML
|
|
45
|
+
<!doctype html>
|
|
46
|
+
<html lang="en">
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="utf-8">
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
50
|
+
<title>Test report unavailable</title>
|
|
51
|
+
<style>
|
|
52
|
+
:root { color-scheme: light dark; }
|
|
53
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
54
|
+
font: 16px/1.6 system-ui, sans-serif; padding: 24px; }
|
|
55
|
+
main { max-width: 34rem; }
|
|
56
|
+
h1 { font-size: 1.25rem; margin: 0 0 .75rem; }
|
|
57
|
+
p { margin: 0 0 .75rem; opacity: .85; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<main>
|
|
62
|
+
<h1>Test report unavailable</h1>
|
|
63
|
+
<p>The last run of the test suite ended before the HTML reporter could
|
|
64
|
+
write a report, so there is nothing to show here yet.</p>
|
|
65
|
+
<p><a href="$RUN_URL">Open the workflow run</a> for the full output.</p>
|
|
66
|
+
<p>Commit $GITHUB_SHA</p>
|
|
67
|
+
</main>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
70
|
+
HTML
|
|
71
|
+
|
|
72
|
+
- name: Deploy to Cloudflare Workers
|
|
73
|
+
working-directory: apps/test-reports
|
|
74
|
+
run: npx wrangler deploy
|
|
75
|
+
env:
|
|
76
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
77
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
78
|
+
|
|
79
|
+
- name: Upload report artifact
|
|
80
|
+
if: always()
|
|
81
|
+
uses: actions/upload-artifact@v4
|
|
82
|
+
with:
|
|
83
|
+
name: test-reports
|
|
84
|
+
path: apps/test-reports/dist/
|
|
85
|
+
if-no-files-found: ignore
|