Skip to content

Commit

Permalink
feat: build workflow for sub-projects (#3317)
Browse files Browse the repository at this point in the history
Creates a working `build` workflow specific to a sub-project when that project has `buildWorkflow` set to `true`.

Based on #3208. So most of the same decisions were made. The `install` command is always run in the root directory same as the `release` workflow. But based on discussion in #3304, we may want to make that configurable. I could make that change in this PR for `build`. Or just address it in a followup PR for both `build` and `release`.
I added a `buildWorkflowName` option to `NodeProject` to support customizing each build workflow name; but it still defaults to `build` or `build_{package-name}` for sub-projects.

I've successfully tested this with my own monorepo and added a few tests.

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • Loading branch information
timothymathison committed Mar 25, 2024
1 parent 4fb188e commit bff36f7
Show file tree
Hide file tree
Showing 24 changed files with 310 additions and 50 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml

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

1 change: 1 addition & 0 deletions .github/workflows/upgrade-bundled-main.yml

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

1 change: 1 addition & 0 deletions .github/workflows/upgrade-main.yml

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

13 changes: 13 additions & 0 deletions docs/api/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ Test whether the given construct is a component.
| <code><a href="#projen.build.BuildWorkflow.property.node">node</a></code> | <code>constructs.Node</code> | The tree node. |
| <code><a href="#projen.build.BuildWorkflow.property.project">project</a></code> | <code>projen.Project</code> | *No description.* |
| <code><a href="#projen.build.BuildWorkflow.property.buildJobIds">buildJobIds</a></code> | <code>string[]</code> | Returns a list of job IDs that are part of the build. |
| <code><a href="#projen.build.BuildWorkflow.property.name">name</a></code> | <code>string</code> | Name of generated github workflow. |

---

Expand Down Expand Up @@ -286,6 +287,18 @@ Returns a list of job IDs that are part of the build.

---

##### `name`<sup>Required</sup> <a name="name" id="projen.build.BuildWorkflow.property.name"></a>

```typescript
public readonly name: string;
```

- *Type:* string

Name of generated github workflow.

---


## Structs <a name="Structs" id="Structs"></a>

Expand Down
44 changes: 36 additions & 8 deletions src/build/build-workflow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import * as path from "path";
import { Task } from "..";
import { Component } from "../component";
import { GitHub, GithubWorkflow, GitIdentity, WorkflowSteps } from "../github";
import {
GitHub,
GithubWorkflow,
GitIdentity,
workflows,
WorkflowSteps,
} from "../github";
import {
BUILD_ARTIFACT_NAME,
DEFAULT_GITHUB_ACTIONS_USER,
Expand All @@ -18,6 +25,8 @@ import {
import { NodeProject } from "../javascript";
import { Project } from "../project";
import { GroupRunnerOptions, filteredRunsOnOptions } from "../runner-options";
import { workflowNameForProject } from "../util/name";
import { ensureRelativePathStartsWithDot } from "../util/path";

const PULL_REQUEST_REF = "${{ github.event.pull_request.head.ref }}";
const PULL_REQUEST_REPOSITORY =
Expand Down Expand Up @@ -123,21 +132,25 @@ export interface BuildWorkflowOptions {
}

export class BuildWorkflow extends Component {
/**
* Name of generated github workflow
*/
public readonly name: string;

private readonly postBuildSteps: JobStep[];
private readonly preBuildSteps: JobStep[];
private readonly gitIdentity: GitIdentity;
private readonly buildTask: Task;
private readonly github: GitHub;
private readonly workflow: GithubWorkflow;
private readonly artifactsDirectory: string;
private readonly name: string;

private readonly _postBuildJobs: string[] = [];

constructor(project: Project, options: BuildWorkflowOptions) {
super(project);

const github = GitHub.of(project);
const github = GitHub.of(this.project.root);
if (!github) {
throw new Error(
"BuildWorkflow is currently only supported for GitHub projects"
Expand All @@ -150,7 +163,7 @@ export class BuildWorkflow extends Component {
this.gitIdentity = options.gitIdentity ?? DEFAULT_GITHUB_ACTIONS_USER;
this.buildTask = options.buildTask;
this.artifactsDirectory = options.artifactsDirectory;
this.name = options.name ?? "build";
this.name = options.name ?? workflowNameForProject("build", this.project);
const mutableBuilds = options.mutableBuild ?? true;

this.workflow = new GithubWorkflow(github, this.name);
Expand All @@ -173,7 +186,11 @@ export class BuildWorkflow extends Component {
}

private addBuildJob(options: BuildWorkflowOptions) {
const jobConfig = {
const projectPathRelativeToRoot = path.relative(
this.project.root.outdir,
this.project.outdir
);
const jobConfig: workflows.Job = {
...filteredRunsOnOptions(options.runsOn, options.runsOnGroup),
container: options.containerImage
? { image: options.containerImage }
Expand All @@ -186,7 +203,16 @@ export class BuildWorkflow extends Component {
contents: JobPermission.WRITE,
...options.permissions,
},
steps: (() => this.renderBuildSteps()) as any,
defaults: this.project.parent // is subproject,
? {
run: {
workingDirectory: ensureRelativePathStartsWithDot(
projectPathRelativeToRoot
),
},
}
: undefined,
steps: (() => this.renderBuildSteps(projectPathRelativeToRoot)) as any,
outputs: {
[SELF_MUTATION_HAPPENED_OUTPUT]: {
stepId: SELF_MUTATION_STEP,
Expand Down Expand Up @@ -378,7 +404,7 @@ export class BuildWorkflow extends Component {
/**
* Called (lazily) during synth to render the build job steps.
*/
private renderBuildSteps(): JobStep[] {
private renderBuildSteps(projectPathRelativeToRoot: string): JobStep[] {
return [
WorkflowSteps.checkout({
with: {
Expand Down Expand Up @@ -417,7 +443,9 @@ export class BuildWorkflow extends Component {
WorkflowSteps.uploadArtifact({
with: {
name: BUILD_ARTIFACT_NAME,
path: this.artifactsDirectory,
path: this.project.parent
? `${projectPathRelativeToRoot}/${this.artifactsDirectory}`
: this.artifactsDirectory,
},
}),
]),
Expand Down
8 changes: 7 additions & 1 deletion src/github/workflow-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ export class WorkflowActions {
"git add .",
`git diff --staged --patch --exit-code > ${GIT_PATCH_FILE} || echo "${options.outputName}=true" >> $GITHUB_OUTPUT`,
].join("\n"),
// always run from root of repository
// overrides default working directory which is set by some workflows using this function
workingDirectory: "./",
},
WorkflowSteps.uploadArtifact({
if: MUTATIONS_FOUND,
name: "Upload patch",
with: { name: GIT_PATCH_FILE, path: GIT_PATCH_FILE },
with: {
name: GIT_PATCH_FILE,
path: GIT_PATCH_FILE,
},
}),
];

Expand Down
7 changes: 5 additions & 2 deletions src/javascript/node-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PROJEN_DIR } from "../common";
import {
AutoMerge,
DependabotOptions,
GitHub,
GitHubProject,
GitHubProjectOptions,
GitIdentity,
Expand Down Expand Up @@ -92,7 +93,6 @@ export interface NodeProjectOptions
* @default - true if not a subproject
*/
readonly buildWorkflow?: boolean;

/**
* Automatically update files modified during builds to pull-request branches. This means
* that any files synthesized by projen or e.g. test snapshots will always be up-to-date
Expand Down Expand Up @@ -593,14 +593,17 @@ export class NodeProject extends GitHubProject {
idToken: requiresIdTokenPermission ? JobPermission.WRITE : undefined,
};

if (buildEnabled && this.github) {
if (buildEnabled && (this.github || GitHub.of(this.root))) {
this.buildWorkflow = new BuildWorkflow(this, {
buildTask: this.buildTask,
artifactsDirectory: this.artifactsDirectory,
containerImage: options.workflowContainerImage,
gitIdentity: this.workflowGitIdentity,
mutableBuild: options.mutableBuild,
preBuildSteps: this.renderWorkflowSetup({
installStepConfiguration: {
workingDirectory: this.determineInstallWorkingDirectory(),
},
mutable: options.mutableBuild ?? true,
}),
postBuildSteps: options.postBuildSteps,
Expand Down
15 changes: 1 addition & 14 deletions src/release/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
filteredWorkflowRunsOnOptions,
} from "../runner-options";
import { Task } from "../task";
import { workflowNameForProject } from "../util/name";
import { ensureRelativePathStartsWithDot } from "../util/path";
import { ReleasableCommits, Version } from "../version";

Expand Down Expand Up @@ -760,20 +761,6 @@ export class Release extends Component {
}
}

function workflowNameForProject(base: string, project: Project): string {
// Subprojects
if (project.parent) {
return `${base}_${fileSafeName(project.name)}`;
}

// root project doesn't get a suffix
return base;
}

function fileSafeName(name: string): string {
return name.replace("@", "").replace(/\//g, "-");
}

/**
* Options for a release branch.
*/
Expand Down
20 changes: 20 additions & 0 deletions src/util/name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Project } from "../project";

/**
* Generate workflow name with suffix based on if project is subproject or not
* @param base name prefix
* @param project to which the workflow belongs
*/
export function workflowNameForProject(base: string, project: Project): string {
// Subprojects
if (project.parent) {
return `${base}_${fileSafeName(project.name)}`;
}

// root project doesn't get a suffix
return base;
}

function fileSafeName(name: string): string {
return name.replace("@", "").replace(/\//g, "-");
}
6 changes: 6 additions & 0 deletions test/__snapshots__/integ.test.ts.snap

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

3 changes: 3 additions & 0 deletions test/awscdk/__snapshots__/awscdk-construct.test.ts.snap

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

0 comments on commit bff36f7

Please sign in to comment.