Skip to content

Commit 4ee547f

Browse files
authored
✨ npm-ci-build + README (#1)
Creates `npm-ci-build~v1` reusable workflow. Documents my “universal targets” thoughts in the README. Documents the `npm-ci-build` workflow. Includes `.editorconfig` to help formatting. Tested with [css-typed#31]. [css-typed#31]: connorjs/css-typed#31
1 parent 44d93a8 commit 4ee547f

File tree

3 files changed

+291
-0
lines changed

3 files changed

+291
-0
lines changed

.editorconfig

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
root = true
2+
3+
[*]
4+
# Prettier default
5+
charset = utf-8
6+
insert_final_newline = true
7+
end_of_line = lf
8+
max_line_length = 80
9+
trim_trailing_whitespace = true
10+
11+
# Custom
12+
# Use tabs: https://alexandersandberg.com/articles/default-to-tabs-instead-of-spaces-for-an-accessible-first-environment/
13+
indent_style = tab
14+
15+
[*.{yaml,yml}]
16+
# YAML disallows tabs: https://yaml.org/spec/
17+
indent_size = 2
18+
indent_style = space
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
on:
2+
workflow_call:
3+
inputs:
4+
hasTests:
5+
description: Whether the project has tests. Used to skip test reporting for projects with no tests.
6+
type: boolean
7+
required: false
8+
default: true
9+
projectType:
10+
description: The type of project. Used to customize the CI build process. MUST be one of `application` or `library`.
11+
type: string
12+
required: false
13+
default: library
14+
outputs:
15+
semVer:
16+
description: The SemVer version number of this build (automated with GitVersion).
17+
value: ${{ jobs.NpmCiBuild.outputs.semVer }}
18+
19+
jobs:
20+
NpmCiBuild:
21+
# Workflow names appear as `origin name / workflow name`, so use `npm` for something like `CI Build / npm`
22+
name: npm
23+
runs-on: ubuntu-latest
24+
outputs:
25+
semVer: ${{ steps.GitVersion.outputs.semVer }}
26+
steps:
27+
- name: Validate inputs
28+
uses: actions/github-script@v7
29+
with:
30+
script: |
31+
const { projectType } = ${{ toJson(inputs) }};
32+
if (!["application", "library"].includes(projectType)) {
33+
core.setFailed(`projectType MUST be one of 'application' or 'library', but was '${projectType}'`);
34+
}
35+
core.info("\u001b[1m\u001b[32mSuccess.");
36+
37+
- name: Checkout self
38+
uses: actions/checkout@v4
39+
with:
40+
fetch-depth: 0 # Full depth (not shallow) for GitVersion
41+
42+
- name: Set up GitVersion
43+
uses: gittools/actions/gitversion/[email protected]
44+
with:
45+
versionSpec: 6.x
46+
47+
- name: Execute GitVersion
48+
id: GitVersion
49+
uses: gittools/actions/gitversion/[email protected]
50+
with:
51+
overrideConfig: |
52+
workflow=GitHubFlow/v1
53+
mode=ContinuousDeployment
54+
55+
- name: Set version
56+
run: sed -i 's/0.0.0-gitversion/${{ env.semVer }}/g' package.json
57+
58+
- name: Use Node.js
59+
uses: actions/setup-node@v4
60+
with:
61+
cache: npm
62+
node-version-file: .node-version
63+
64+
- name: Install
65+
run: npm ci --strict-peer-deps
66+
67+
- name: CI build
68+
run: npm run ci-build
69+
70+
# TODO: Test artifacts, using inputs.hasTests
71+
# TODO: Publish pack artifact, using inputs.projectType

README.md

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<div align="center">
2+
3+
<h1>GitHub Workflows</h1>
4+
5+
My GitHub workflows. Centralized for reuse.
6+
7+
</div>
8+
9+
**Table of contents**
10+
11+
- [Published workflows](#published-workflows)
12+
- [npm-ci-build](#npm-ci-build)
13+
- [Goal: Universal targets](#goal-universal-targets)
14+
- [Industry observations](#industry-observations)
15+
- [My universal targets](#my-universal-targets)
16+
- [My universal output locations](#my-universal-output-locations)
17+
- [Style conventions](#style-conventions)
18+
19+
## Published workflows
20+
21+
> [!NOTE]
22+
>
23+
> The workflows use file versioning (`name~v1.yaml`) instead of repository tags.
24+
>
25+
> <details><summary>Why?</summary><div>
26+
>
27+
> This provides independent versioning of the workflows while maintaining them in one repository.
28+
> It also allows supporting older versions on the <code>main</code> branch (for example, upgrading all checkout actions).
29+
>
30+
> File versioning uses the tilde (`~`) because GitHub prohibits `@` in workflow file names.
31+
> (GitHub uses `@` for repository references.)
32+
> The tilde still reads well, displays well in URLs, and is not often used in file names.
33+
>
34+
> </div></details>
35+
36+
### npm-ci-build
37+
38+
Job that executes `ci-build` logic for npm packages.
39+
40+
```yaml
41+
jobs:
42+
ci-build:
43+
uses: connorjs/github-workflows/.github/workflows/npm-ci-build~v1.yaml
44+
```
45+
46+
`v1` runs `ci-build` directly and assumes that the underlying package orchestrates the full build correctly.
47+
48+
<details>
49+
<summary>Show inputs, outputs, assumptions, and notes</summary>
50+
51+
#### Inputs
52+
53+
| Name | Default | Type | Description |
54+
|:-------------:|:---------:|:-------:|:-------------------------------------------------------------------------------------------:|
55+
| `hasTests` | `true` | boolean | Whether the project has tests.<br>Used to skip test reporting for projects with no tests. |
56+
| `projectType` | `library` | choice | The type of project: `application` or `library`.<br>Used to customize the CI build process. |
57+
58+
#### Outputs
59+
60+
| Name | Description |
61+
|:--------:|:--------------------------------------------------------------------:|
62+
| `semVer` | The SemVer version number of this build (automated with GitVersion). |
63+
64+
#### Assumptions
65+
66+
The job makes the following assumptions about the repository.
67+
68+
- Uses a `.node-version` file in the root of the repository to set the Node.js version.
69+
- Uses npm (not yarn or pnpm).
70+
- The `package.json` uses `0.0.0-gitversion` for its version (enables GitVersion automation).
71+
72+
#### Notes
73+
74+
The job includes automatic versioning via GitVersion.
75+
It will replace `0.0.0-gitversion` with the correct version.
76+
_Note: While another tool could replace GitVersion, the automatic version string will remain `0.0.0-gitversion`._
77+
78+
Note: The job will ignore any local `GitVersion.yaml`; it configures GitVersion for continuous deployment internally.
79+
Use `+semver:(major|minor)` in commit messages appropriately.
80+
(Patch happens automatically.)
81+
82+
</details>
83+
84+
## Goal: Universal targets
85+
86+
I find that a universal set of targets (also called tasks) simplifies polyglot development.
87+
I observed the benefits with Amazon’s internal build system, and I built similar mechanisms at Total Quality Logistics (TQL).
88+
89+
This repository represents my personal take on the idea, implemented via a consistent set of GitHub workflows.
90+
91+
### Industry observations
92+
93+
This section details my observations from industry tools that I have used.
94+
95+
- Amazon: Standardized install, build, test (unit-test and integration-test) all under release.
96+
(Source: per my memory 18+ months later)
97+
98+
- Gradle: Standardizes check, assemble, and build tasks.
99+
Includes some sort of standardized dependency installation (but I cannot find the task name).
100+
Specific plugins (such as java) extend with their own standard set (compile, javadoc, and more).
101+
(Source: [base plugin](https://docs.gradle.org/current/userguide/base_plugin.html))
102+
103+
- Maven: Standardizes validate, compile, test, package, verify, install, and deploy
104+
(Source: [Maven build lifecycle](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html))
105+
106+
- .NET (`dotnet`): Restore, build, test, format, run, and publish.
107+
(Source: My limited usage and [dotnet commands](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet))
108+
109+
- npm: Standardizes install/ci/rebuild, start/stop/restart, test, and many related to prepare and publish.
110+
Many libraries suggest build and lint, but there are others related (compile, tsc, specific linters, and formatters).
111+
(Source: My experience and [npm lifecycle scripts](https://docs.npmjs.com/cli/v10/using-npm/scripts))
112+
113+
- Python: Unsure.
114+
I know of install from pipenv.
115+
I see build, fmt, and test from hatch, install and build from flit, and easy_install and build from Setuptools.
116+
117+
Each of these also had the concept of clean.
118+
119+
### My universal targets
120+
121+
Combining my experiences predominantly with Java, npm, and .NET, I arrive at the following universal targets.
122+
Each underlying build system should be able to hook into these standardized targets.
123+
124+
- `install`: Installs (or restores) dependencies.
125+
126+
- `build`: Builds the project.
127+
128+
- `format`: Formats and lints the project.
129+
130+
- `test`: Runs all tests that do not require a network connection.
131+
Often thought of as unit tests, but can contain network-free integration tests (aka component or module tests).
132+
This should include code coverage.
133+
134+
- `publish`: Publish the library to its package manager.
135+
136+
- `version`: Handles automatic versioning, if desired.
137+
138+
- `clean`: Cleans any build or test artifacts.
139+
SHOULD NOT remove dependencies.
140+
141+
In CI (think code reviews or pull requests), the “build” executes many of these targets.
142+
Using the term “build” here would overload the target name though.
143+
Therefore, I use the term `ci-build` to group all targets that should run during the “CI Build.”
144+
145+
We called this `release` at Amazon, but I have found that folks think `release` means “make the release; actually publish,” which represents the wrong conclusion.
146+
Hence, I chose the term `ci-build`.
147+
_(I also considered terms such as `full-build`, `release-build`, and `ci` or replacing the underlying `build` term with `compile`.
148+
However, others came to the right conclusion with `ci-build`, which reinforces its choice.)_
149+
150+
If I was building my own CLI that wrapped underlying tools with these universal targets, I would include `compile` and `lint` as aliases for `build` and `format`.
151+
If both targets were present, the main name would take precedence.
152+
153+
### My universal output locations
154+
155+
Standardizing the output locations also simplifies polyglot development.
156+
While tooling (GitHub workflows) receives the most benefits, developers can benefit from standardized code coverage output locations.
157+
Standardized outputs can also simplify `.gitignore` configuration.
158+
_(Nit-pick side note: The 100s-lines-long default `.gitignore` files pain me.)_
159+
160+
The standard output locations follow.
161+
162+
- `artifacts` should contain as much output as possible.
163+
This also simplifies clean.
164+
165+
- Dependencies will use underlying tool conventions (example: `node_modules` for npm).
166+
167+
- Publish will use underlying tool conventions where appropriate.
168+
Use `artifacts/dist` to vend transpiled sources.
169+
170+
- Mono-repos will use `$projectName` in the path per artifact type (example: `artifacts/dist/$projectName`).
171+
This preserves output structure semantics between polyrepo (single project repositories) and monorepo use cases.
172+
173+
- Tests will use `artifacts/test-results`.
174+
Prefer JUnit format for the test results file and use the name `test-result.xml`.
175+
Prefer Cobertura format for the code coverage results file and use the name `cobertura.xml`.
176+
177+
In monorepos, each project likely has these files in `artifacts/test-results/$projectName` _AND_ the monorepo workspace aggregate in `artifacts/test-results` (N+1 output files).
178+
Note that tools like [reportgenerator](https://reportgenerator.io) exist to simplify aggregation.
179+
180+
- The HTML output from code coverage should have the name `artifacts/coverage/index.html`.
181+
This differs from `artifacts/test-results` to simplify developers opening the report and to prevent conflicts in monorepos.
182+
183+
## Style conventions
184+
185+
Some notes on how I style (format) my GitHub workflow files.
186+
187+
- The following naming rules ensure seamless script usage, such as in `actions/github-script`.
188+
- Prefer `PascalCase` for job and step IDs.
189+
- Prefer `camelCase` for variable names (inputs and outputs).
190+
- _This means avoid `snake_case`, `kebab-case`, `TRAIN-CASE`, or `SCREAM_CASE` unless a 3rd party uses them._
191+
192+
- Format expressions with inner spaces: `${{ foo.bar }}`.
193+
194+
- Prefer no quotes if YAML does not require them.
195+
196+
- Automatically format files: Use prettier, an editor using .editorconfig, or a similar formatter.
197+
198+
- Start each step with `name`.
199+
- Use sentence case for `name` (with allowed use of Proper Nouns when it helps such as GitVersion).
200+
- Exception: Prefer “CI Build” (instead of “CI build” or “ci build”).
201+
202+
- If a step (or job) would benefit from a longer description, include a YAML comment on the line after `name` (unless GitHub adds proper `description` field).

0 commit comments

Comments
 (0)