Skip to content
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ name: ci
on:
pull_request:
push:
branches: [main]
branches:
- main
workflow_dispatch:

permissions:
contents: read
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ on:
major:
description: Major version of the release
value: ${{ jobs.release-please.outputs.major }}
pr_branch:
description: Branch of the release pull request this run opened or updated with GITHUB_TOKEN, which starts no workflows; empty otherwise
value: ${{ jobs.release-please.outputs.pr_branch }}

permissions:
contents: read
Expand All @@ -35,10 +38,22 @@ jobs:
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}
major: ${{ steps.release.outputs.major }}
pr_branch: ${{ steps.pr.outputs.branch }}
steps:
- id: release
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ secrets.token || github.token }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
- name: Release pull request branch
id: pr
if: steps.release.outputs.prs_created == 'true'
env:
PR: ${{ steps.release.outputs.pr }}
OWN_TOKEN: ${{ secrets.token != '' }}
run: |
# a pull request opened with the caller's own token starts CI by itself
if [ "$OWN_TOKEN" = "false" ]; then
echo "branch=$(jq -r .headBranchName <<<"$PR")" >> "$GITHUB_OUTPUT"
fi
11 changes: 11 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,14 @@ jobs:
git fetch --force origin "refs/tags/$TAG:refs/tags/$TAG"
git tag -f "v$MAJOR" "$TAG"
git push -f origin "refs/tags/v$MAJOR"
release-pr-ci:
needs: release
if: ${{ needs.release.outputs.pr_branch != '' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ needs.release.outputs.pr_branch }}
run: gh workflow run ci.yml --repo "$GITHUB_REPOSITORY" --ref "$BRANCH"
2 changes: 1 addition & 1 deletion .repokeeper.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# repokeeper configuration: https://github.com/vannt-dev/repokeeper
schema: 1
standard: 1.3.2
standard: 1.4.0
platform: github
stacks:
- node
Expand Down
17 changes: 15 additions & 2 deletions .repokeeper/lock.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"lockVersion": 1,
"standard": "1.3.2",
"standard": "1.4.0",
"entries": [
{
"id": "block:.gitattributes#editorconfig",
Expand Down Expand Up @@ -251,7 +251,7 @@
{
"id": "yaml:.github/workflows/ci.yml#[\"on\"]",
"module": "ci",
"hash": "8a324c0626f6d862860cca96e075031faea73ddbc51d230f2c6ea2d54088c164",
"hash": "6ba4774ab2129a605bb160c5ee6f48ebc51544881592ad60ad5268b910fcfbde",
"target": {
"kind": "yaml",
"path": ".github/workflows/ci.yml",
Expand All @@ -272,6 +272,19 @@
]
}
},
{
"id": "yaml:.github/workflows/release.yml#[\"jobs\",\"release-pr-ci\"]",
"module": "release",
"hash": "5fe7f3717447417a686ad02836df7bf9bb3225fb37358364fd13a11251f997c2",
"target": {
"kind": "yaml",
"path": ".github/workflows/release.yml",
"keyPath": [
"jobs",
"release-pr-ci"
]
}
},
{
"id": "yaml:.github/workflows/release.yml#[\"jobs\",\"release\"]",
"module": "release",
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ work:
- In the repository settings, under Actions → General, allow GitHub Actions to create and approve
pull requests.
- Optionally add a `RELEASE_PLEASE_TOKEN` secret (a fine-grained token with contents, pull requests
and issues write access). Without it the release pull request is opened with `GITHUB_TOKEN`, and
GitHub does not run CI on pull requests opened that way.
and issues write access). Without it the release pull request is opened with `GITHUB_TOKEN`, which
starts no workflows, so the `release-pr-ci` job runs `ci.yml` on the release branch itself; its
checks then satisfy required checks in a ruleset.

A repository with no release yet (manifest at `0.0.0`) gets `initial-version: 0.1.0`, so its first
release is 0.1.0 rather than release-please's default 1.0.0.

Set `modules.drift: true` to add a `repokeeper` job to `ci.yml` that runs `repokeeper check` with
the version that wrote the standard, so a pull request that edits a managed file fails until the edit
Expand Down
36 changes: 34 additions & 2 deletions src/platforms/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,12 @@ export const githubPlatform: PlatformAdapter = {
if (jobs.length === 0) return [];
return [
workflowKey("ci", path, ["name"], "ci"),
workflowKey("ci", path, ["on"], { pull_request: null, push: { branches: [defaultBranch(ctx.config)] } }),
// workflow_dispatch lets the release workflow run CI on release pull requests (see releaseAutomation)
workflowKey("ci", path, ["on"], {
pull_request: null,
push: { branches: [defaultBranch(ctx.config)] },
workflow_dispatch: null,
}),
workflowKey("ci", path, ["permissions"], { contents: "read" }),
// a new push to a pull request makes its earlier run pointless; runs on the default branch always finish
workflowKey("ci", path, ["concurrency"], {
Expand All @@ -159,6 +164,8 @@ export const githubPlatform: PlatformAdapter = {

releaseAutomation(ctx: ModuleContext, release: ReleaseInfo): Output[] {
const path = ".github/workflows/release.yml";
const seed = release.version ?? ctx.repo.releasedVersion ?? "0.0.0";
const hasCi = ctx.config.modules.ci && githubPlatform.ciWorkflow(ctx).length > 0;
return [
{
kind: "file",
Expand All @@ -176,6 +183,8 @@ export const githubPlatform: PlatformAdapter = {
...(release.versionFile ? { "version-file": release.versionFile } : {}),
// Without it a pom at a release version first gets a pull request that only bumps to -SNAPSHOT
...(release.type === "maven" ? { "skip-snapshot": true } : {}),
// release-please makes a repository's first release 1.0.0 unless told otherwise
...(seed === "0.0.0" ? { "initial-version": "0.1.0" } : {}),
},
},
}),
Expand All @@ -184,7 +193,7 @@ export const githubPlatform: PlatformAdapter = {
kind: "seed",
module: "release",
path: ".release-please-manifest.json",
content: json({ ".": release.version ?? ctx.repo.releasedVersion ?? "0.0.0" }),
content: json({ ".": seed }),
},
workflowKey("release", path, ["name"], "release"),
workflowKey("release", path, ["on"], { push: { branches: [defaultBranch(ctx.config)] } }),
Expand All @@ -195,6 +204,29 @@ export const githubPlatform: PlatformAdapter = {
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitHub Actions expression, not a JS template
secrets: { token: "${{ secrets.RELEASE_PLEASE_TOKEN }}" },
}),
// Pull requests opened with GITHUB_TOKEN start no workflows, so required checks would never report
...(hasCi
? [
workflowKey("release", path, ["jobs", "release-pr-ci"], {
needs: "release",
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitHub Actions expression, not a JS template
if: "${{ needs.release.outputs.pr_branch != '' }}",
"runs-on": "ubuntu-latest",
permissions: { actions: "write" },
steps: [
{
env: {
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitHub Actions expression, not a JS template
GH_TOKEN: "${{ github.token }}",
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitHub Actions expression, not a JS template
BRANCH: "${{ needs.release.outputs.pr_branch }}",
},
run: 'gh workflow run ci.yml --repo "$GITHUB_REPOSITORY" --ref "$BRANCH"',
},
],
}),
]
: []),
];
},
};
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const PACKAGE_VERSION: string = (
).version;

/** The standard this build of repokeeper applies. Bump it whenever generated output changes. */
export const STANDARD_VERSION = "1.3.2";
export const STANDARD_VERSION = "1.4.0";

export const TOOL_VERSIONS = {
lefthook: "2.1.14",
Expand Down
44 changes: 42 additions & 2 deletions test/modules-ci-release.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("ci module", () => {
it("calls the reusable workflows at the moving major tag", () => {
const out = keys(ciModule.outputs(makeContext()));
expect(out.name).toBe("ci");
expect(out.on).toEqual({ pull_request: null, push: { branches: ["main"] } });
expect(out.on).toEqual({ pull_request: null, push: { branches: ["main"] }, workflow_dispatch: null });
expect(out.permissions).toEqual({ contents: "read" });
expect(out["jobs.node"]).toEqual({
uses: `vannt-dev/repokeeper/.github/workflows/stack-node.yml@${WORKFLOW_REF}`,
Expand All @@ -35,7 +35,7 @@ describe("ci module", () => {
it("follows the configured default branch and drops the commits job with the commits module", () => {
const ctx = makeContext({ config: { github: { default_branch: "trunk" } }, modules: { commits: false } });
const out = keys(ciModule.outputs(ctx));
expect(out.on).toEqual({ pull_request: null, push: { branches: ["trunk"] } });
expect(out.on).toEqual({ pull_request: null, push: { branches: ["trunk"] }, workflow_dispatch: null });
expect(out["jobs.commits"]).toBeUndefined();
});

Expand Down Expand Up @@ -105,6 +105,46 @@ describe("release module", () => {
});
});

it("starts a repository with no release yet at 0.1.0 instead of release-please's 1.0.0", () => {
const pkg = (version: string | null) => {
const outputs = releaseModule.outputs(
makeContext({ stacks: [nodeResolved({ release: { type: "simple", version } })] }),
);
const config = outputs.find((o) => o.path === "release-please-config.json");
return JSON.parse(config?.kind === "file" ? config.content : "").packages["."];
};
expect(pkg(null)["initial-version"]).toBe("0.1.0");
expect(pkg("0.3.0")).not.toHaveProperty("initial-version");
});

it("runs CI on release pull requests opened with GITHUB_TOKEN, since those start no workflows", () => {
const out = keys(releaseModule.outputs(makeContext()));
expect(out["jobs.release-pr-ci"]).toEqual({
needs: "release",
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitHub Actions expression, not a JS template
if: "${{ needs.release.outputs.pr_branch != '' }}",
"runs-on": "ubuntu-latest",
permissions: { actions: "write" },
steps: [
{
env: {
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitHub Actions expression, not a JS template
GH_TOKEN: "${{ github.token }}",
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitHub Actions expression, not a JS template
BRANCH: "${{ needs.release.outputs.pr_branch }}",
},
run: 'gh workflow run ci.yml --repo "$GITHUB_REPOSITORY" --ref "$BRANCH"',
},
],
});
});

it("leaves that job out when the repository has no ci workflow", () => {
expect(keys(releaseModule.outputs(makeContext({ modules: { ci: false } })))).not.toHaveProperty(
"jobs.release-pr-ci",
);
});

it("prefers a language release type and falls back to simple at 0.0.0", () => {
const simple = nodeResolved({ id: "node", release: { type: "simple", version: null } });
expect(pickRelease([simple, nodeResolved({ release: { type: "node", version: "2.0.0" } })])).toEqual({
Expand Down
14 changes: 13 additions & 1 deletion test/workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type { StackPack } from "../src/stacks/types.js";
import { TOOL_VERSIONS } from "../src/version.js";

interface Step {
if?: string;
env?: Record<string, string>;
name?: string;
uses?: string;
run?: string;
Expand Down Expand Up @@ -182,6 +184,16 @@ describe("commitlint config in the reusable workflow", () => {

it("release-please exposes the outputs callers use", () => {
expect(Object.keys(workflow("release-please.yml").on.workflow_call?.outputs ?? {})).toEqual(
expect.arrayContaining(["release_created", "tag_name", "version", "major"]),
expect.arrayContaining(["release_created", "tag_name", "version", "major", "pr_branch"]),
);
});

it("release-please names the release branch only when GITHUB_TOKEN opened the pull request", () => {
const steps = workflow("release-please.yml").jobs["release-please"]?.steps ?? [];
const pr = steps.find((s) => s.name === "Release pull request branch");
expect(pr?.if).toBe("steps.release.outputs.prs_created == 'true'");
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitHub Actions expression, not a JS template
expect(pr?.env).toMatchObject({ OWN_TOKEN: "${{ secrets.token != '' }}" });
expect(pr?.run).toContain(`if [ "$OWN_TOKEN" = "false" ]`);
expect(pr?.run).toContain("jq -r .headBranchName");
});
Loading