Skip to content

Commit 8ff748b

Browse files
committed
docker build feature, docs, release improvements
1 parent fe7ac78 commit 8ff748b

File tree

27 files changed

+1354
-65
lines changed

27 files changed

+1354
-65
lines changed

.biomeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.github/wait-for-checks.js

.github/wait-for-checks.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* Wait for GitHub Actions checks to complete on the current commit
3+
*
4+
* Tailored for the cigen repo: waits for CI and Docs checks.
5+
*
6+
* @param {Object} params
7+
* @param {Object} params.github - GitHub API object
8+
* @param {Object} params.context - GitHub context
9+
* @param {Object} params.core - GitHub Actions core
10+
* @param {Array<string>} params.checks - Optional array of check name prefixes to wait for
11+
*/
12+
13+
const DEFAULT_REQUIRED_PREFIXES = [
14+
"Test", // CI job name from ci.yml
15+
"CI Gate", // first job in docs.yml
16+
"Build Docs", // docs build job
17+
"Deploy Docs", // docs deploy job
18+
];
19+
20+
const SEC = 1000; // milliseconds in a second
21+
/* biome-ignore lint/style/noMagicNumbers: configuring timeout in minutes */
22+
const TIMEOUT_MS = 30 * 60 * SEC; // 30 minutes overall timeout
23+
/* biome-ignore lint/style/noMagicNumbers: configuring warmup in minutes */
24+
const WARMUP_MS = 5 * 60 * SEC; // 5 minutes for checks to appear
25+
const POLL_INTERVAL_MS = 10 * SEC; // 10 seconds between polls
26+
/* biome-ignore lint/style/noMagicNumbers: configuring retry sleep seconds */
27+
const RETRY_SLEEP_MS = 5 * SEC; // retry interval during warmup
28+
29+
function sleep(ms) {
30+
return new Promise((resolve) => setTimeout(resolve, ms));
31+
}
32+
33+
async function listChecks(github, context) {
34+
const { data } = await github.rest.checks.listForRef({
35+
owner: context.repo.owner,
36+
repo: context.repo.repo,
37+
ref: context.sha,
38+
per_page: 100,
39+
});
40+
return data.check_runs.map((c) => ({
41+
name: c.name,
42+
status: c.status, // queued, in_progress, completed
43+
conclusion: c.conclusion, // success, failure, neutral, cancelled, etc
44+
}));
45+
}
46+
47+
function makeMatcher(requiredPrefixes) {
48+
return (name) => requiredPrefixes.some((prefix) => name.startsWith(prefix));
49+
}
50+
51+
async function discoverPresentChecks({
52+
github,
53+
context,
54+
core,
55+
requiredPrefixes,
56+
}) {
57+
core.info("Discovering present checks...");
58+
const matchesRequired = makeMatcher(requiredPrefixes);
59+
let presentChecks = [];
60+
const warmupStart = Date.now();
61+
62+
while (Date.now() - warmupStart < WARMUP_MS) {
63+
const listedChecks = await listChecks(github, context);
64+
presentChecks = listedChecks.filter((c) => matchesRequired(c.name));
65+
66+
if (presentChecks.length > 0) {
67+
core.info(
68+
`Found ${presentChecks.length} required check(s): ${presentChecks
69+
.map((c) => c.name)
70+
.join(", ")}`
71+
);
72+
break;
73+
}
74+
75+
core.info(
76+
`No required checks found yet, waiting... (${Math.round(
77+
(Date.now() - warmupStart) / SEC
78+
)}s elapsed)`
79+
);
80+
await sleep(RETRY_SLEEP_MS);
81+
}
82+
83+
return presentChecks;
84+
}
85+
86+
async function waitForRequiredChecks({
87+
github,
88+
context,
89+
core,
90+
requiredPrefixes,
91+
presentChecks,
92+
}) {
93+
const matchesRequired = makeMatcher(requiredPrefixes);
94+
core.info(`Waiting for ${presentChecks.length} check(s) to complete...`);
95+
const waitStart = Date.now();
96+
97+
while (Date.now() - waitStart < TIMEOUT_MS) {
98+
const listedChecks = await listChecks(github, context);
99+
const relevant = listedChecks.filter((c) => matchesRequired(c.name));
100+
101+
// If checks disappeared (canceled?), keep waiting within timeout
102+
if (relevant.length === 0) {
103+
core.warning("Required checks disappeared - they may have been canceled");
104+
await sleep(POLL_INTERVAL_MS);
105+
continue;
106+
}
107+
108+
const pending = relevant.filter((c) => c.status !== "completed");
109+
110+
if (pending.length === 0) {
111+
// All checks completed - verify conclusions
112+
const failed = relevant.filter(
113+
(c) => c.conclusion !== "success" && c.conclusion !== "skipped"
114+
);
115+
116+
if (failed.length > 0) {
117+
const failureDetails = failed
118+
.map((f) => `${f.name} (${f.conclusion})`)
119+
.join(", ");
120+
core.setFailed(`Some required checks failed: ${failureDetails}`);
121+
process.exit(1);
122+
}
123+
124+
const successful = relevant.filter((c) => c.conclusion === "success");
125+
core.info(
126+
`✅ All ${successful.length} required check(s) passed successfully!`
127+
);
128+
return;
129+
}
130+
131+
const elapsed = Math.round((Date.now() - waitStart) / SEC);
132+
const pendingDetails = pending
133+
.map((p) => `${p.name} (${p.status})`)
134+
.join(", ");
135+
core.info(`[${elapsed}s] Waiting for: ${pendingDetails}`);
136+
137+
await sleep(POLL_INTERVAL_MS);
138+
}
139+
140+
core.setFailed(
141+
`Timeout after ${Math.round(TIMEOUT_MS / SEC)}s waiting for checks to complete`
142+
);
143+
process.exit(1);
144+
}
145+
146+
module.exports = async ({ github, context, core, checks }) => {
147+
const REQUIRED_PREFIXES = checks?.length ? checks : DEFAULT_REQUIRED_PREFIXES;
148+
149+
if (checks?.length) {
150+
core.info(`Waiting for specific checks: ${checks.join(", ")}`);
151+
} else {
152+
core.info("Waiting for default required checks (CI + Docs)");
153+
}
154+
155+
const presentChecks = await discoverPresentChecks({
156+
github,
157+
context,
158+
core,
159+
requiredPrefixes: REQUIRED_PREFIXES,
160+
});
161+
162+
if (presentChecks.length === 0) {
163+
core.info(
164+
"No required checks present on this commit - continuing without waiting."
165+
);
166+
core.info("This can happen if workflows were skipped by path filters.");
167+
return;
168+
}
169+
170+
await waitForRequiredChecks({
171+
github,
172+
context,
173+
core,
174+
requiredPrefixes: REQUIRED_PREFIXES,
175+
presentChecks,
176+
});
177+
};

.github/workflows/release.yml

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*" # Version tags like v1.2.3
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
release_build:
14+
name: Build ${{ matrix.name }}
15+
runs-on: ${{ matrix.os }}
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
include:
20+
# macOS builds
21+
- os: macos-latest
22+
target: x86_64-apple-darwin
23+
name: cigen-macos-amd64
24+
- os: macos-latest
25+
target: aarch64-apple-darwin
26+
name: cigen-macos-arm64
27+
28+
# Linux builds
29+
- os: ubuntu-latest
30+
target: x86_64-unknown-linux-gnu
31+
name: cigen-linux-amd64
32+
- os: ubuntu-latest
33+
target: aarch64-unknown-linux-gnu
34+
name: cigen-linux-arm64
35+
use-cross: true
36+
37+
# Windows builds
38+
- os: windows-latest
39+
target: x86_64-pc-windows-msvc
40+
name: cigen-windows-amd64
41+
- os: windows-latest
42+
target: aarch64-pc-windows-msvc
43+
name: cigen-windows-arm64
44+
45+
steps:
46+
- name: Checkout repository
47+
uses: actions/checkout@v4
48+
with:
49+
fetch-depth: 0
50+
fetch-tags: true
51+
52+
- name: Wait for required checks
53+
uses: actions/github-script@v7
54+
with:
55+
script: |
56+
const script = require('./.github/wait-for-checks.js');
57+
await script({ github, context, core });
58+
59+
- name: Install Rust
60+
uses: dtolnay/rust-toolchain@stable
61+
with:
62+
targets: ${{ matrix.target }}
63+
64+
- name: Add rust target (conditional)
65+
if: (matrix.target == 'x86_64-apple-darwin' && matrix.os == 'macos-latest') || (matrix.target == 'aarch64-pc-windows-msvc' && matrix.os == 'windows-latest')
66+
run: rustup target add ${{ matrix.target }}
67+
68+
- name: Install cross (for cross-compilation)
69+
if: matrix.use-cross
70+
run: cargo install cross --git https://github.com/cross-rs/cross
71+
72+
- name: Build binary
73+
shell: bash
74+
run: |
75+
set -e
76+
if [ "${{ matrix.use-cross }}" = "true" ]; then
77+
cross build --release --target ${{ matrix.target }} --bin cigen
78+
else
79+
cargo build --release --target ${{ matrix.target }} --bin cigen
80+
fi
81+
82+
- name: Create archive
83+
shell: bash
84+
run: |
85+
set -e
86+
cd "target/${{ matrix.target }}/release"
87+
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
88+
7z a "../../../${{ matrix.name }}.zip" cigen.exe
89+
cd ../../../
90+
echo "ASSET_PATH=${{ matrix.name }}.zip" >> "$GITHUB_ENV"
91+
else
92+
tar czf "../../../${{ matrix.name }}.tar.gz" cigen
93+
cd ../../../
94+
echo "ASSET_PATH=${{ matrix.name }}.tar.gz" >> "$GITHUB_ENV"
95+
fi
96+
97+
- name: Upload artifact
98+
uses: actions/upload-artifact@v4
99+
with:
100+
name: ${{ matrix.name }}
101+
path: ${{ env.ASSET_PATH }}
102+
103+
release_create:
104+
name: Create Release
105+
needs: release_build
106+
runs-on: ubuntu-latest
107+
108+
steps:
109+
- name: Checkout repository
110+
uses: actions/checkout@v4
111+
with:
112+
fetch-depth: 0
113+
fetch-tags: true
114+
115+
- name: Download artifacts
116+
uses: actions/download-artifact@v4
117+
with:
118+
path: artifacts
119+
120+
- name: Get version from Cargo.toml
121+
id: version
122+
run: |
123+
VERSION=$(grep -E '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
124+
{
125+
echo "version=$VERSION"
126+
} >> "$GITHUB_OUTPUT"
127+
128+
if [ "${{ github.event_name }}" = "push" ]; then
129+
TAG="${GITHUB_REF#refs/tags/}"
130+
EXPECTED_TAG="v$VERSION"
131+
if [ "$TAG" != "$EXPECTED_TAG" ]; then
132+
echo "Error: Tag $TAG doesn't match expected $EXPECTED_TAG from Cargo.toml" >&2
133+
exit 1
134+
fi
135+
{
136+
echo "tag=$TAG"
137+
} >> "$GITHUB_OUTPUT"
138+
else
139+
{
140+
echo "tag=v$VERSION"
141+
} >> "$GITHUB_OUTPUT"
142+
fi
143+
144+
- name: Generate checksums
145+
shell: bash
146+
run: |
147+
set -e
148+
cd artifacts
149+
for dir in */; do
150+
cd "$dir"
151+
for file in *; do
152+
case "$file" in
153+
*.tar.gz|*.zip)
154+
if [ -f "$file" ]; then
155+
sha256sum "$file" > "${file}.sha256" || shasum -a 256 "$file" | awk '{print $1}' > "${file}.sha256"
156+
fi
157+
;;
158+
esac
159+
done
160+
cd ..
161+
done
162+
cd ..
163+
164+
- name: Generate changelog
165+
id: changelog
166+
run: |
167+
cat > changelog.md <<EOF
168+
## Installation
169+
170+
### One-liner (Linux/macOS)
171+
curl -fsSL https://docspring.github.io/cigen/install.sh | sh
172+
173+
### Direct downloads
174+
- macOS (Intel): https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.tag }}/cigen-macos-amd64.tar.gz
175+
- macOS (Apple Silicon): https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.tag }}/cigen-macos-arm64.tar.gz
176+
- Linux (x86_64): https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.tag }}/cigen-linux-amd64.tar.gz
177+
- Linux (ARM64): https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.tag }}/cigen-linux-arm64.tar.gz
178+
- Windows (x86_64): https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.tag }}/cigen-windows-amd64.zip
179+
- Windows (ARM64): https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.tag }}/cigen-windows-arm64.zip
180+
EOF
181+
182+
- name: Create GitHub Release
183+
uses: softprops/action-gh-release@v2
184+
with:
185+
tag_name: ${{ steps.version.outputs.tag }}
186+
name: CIGen v${{ steps.version.outputs.version }}
187+
body_path: changelog.md
188+
draft: false
189+
prerelease: ${{ contains(steps.version.outputs.tag, '-') }}
190+
files: |
191+
artifacts/**/*.tar.gz
192+
artifacts/**/*.zip
193+
artifacts/**/*.sha256

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ tracing = "0.1.41"
3131
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
3232
which = "8.0.0"
3333
yaml-spanned = "0.0.2"
34+
globwalk = "0.9.1"
35+
walkdir = "2.5.0"
3436

3537
[dev-dependencies]
3638
assert_cmd = "2.0.17"

0 commit comments

Comments
 (0)