Skip to content

Commit 637a784

Browse files
committed
Add release github action, and a few fixes to wait cmd
1 parent 53f4af0 commit 637a784

7 files changed

Lines changed: 128 additions & 25 deletions

File tree

.github/workflows/actions.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
name: Quality Assurance
2+
name: Actions
33

44
on:
55
push:
@@ -40,7 +40,7 @@ jobs:
4040
with:
4141
node-version: 22
4242
cache: 'npm'
43-
- run: npm ci --no-audit --no-fund
43+
- run: npm install --no-audit --no-fund --cache .npm
4444
- run: npx tsc
4545
- run: npm test
4646

@@ -52,8 +52,7 @@ jobs:
5252
with:
5353
node-version: 22
5454
cache: 'npm'
55+
- run: npm install --no-audit --no-fund --cache .npm
5556
- run: npx tsc
5657
- run: node src/index.js generate-schema > schema.json
57-
- run: |
58-
git diff --exit-code -- schema.json || (>&2 echo "schema.json is not up to date. Please run node src/index.js generate-schema > schema.json"; exit 1)
59-
58+
- run: git diff --exit-code -- schema.json || (>&2 echo "schema.json is not up to date. Please run node src/index.js generate-schema > schema.json"; exit 1)

.github/workflows/release.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
on:
2+
release:
3+
types: [ created ]
4+
5+
permissions:
6+
contents: write
7+
packages: write
8+
9+
jobs:
10+
release-docker-image:
11+
name: Release docker image
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
platform: [ amd64, arm64 ]
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: docker/setup-qemu-action@v3
19+
- uses: docker/setup-buildx-action@v3
20+
- uses: docker/login-action@v1
21+
with:
22+
registry: ghcr.io
23+
username: ${{github.actor}}
24+
password: ${{secrets.GITHUB_TOKEN}}
25+
- uses: docker/build-push-action@v6
26+
with:
27+
platforms: linux/amd64,linux/arm64
28+
push: true
29+
tags: ghcr.io/firecow/swarm-app:latest

Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM node:22.15.1-alpine
2+
3+
WORKDIR /app/
4+
COPY package*.json /app/
5+
RUN npm install
6+
7+
COPY src /app/src
8+
9+
ENTRYPOINT ["node", "src/index.js"]

Dockerfile.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*
2+
!*.json
3+
!src/

src/commands/deploy-cmd.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import {ArgumentsCamelCase, Argv} from "yargs";
22
import {createMissingConfigs, createMissingNetworks, removeUnusedConfigs, removeUnusedServices, upsertServices} from "../docker-api.js";
33
import {initContext} from "../context.js";
4+
import timers from "timers/promises";
45

56
export const command = "deploy <app-name>";
67
export const description = "Deploys swarm app";
78

89
export async function handler (args: ArgumentsCamelCase) {
910
const ctx = await initContext(args);
11+
const dockerode = ctx.dockerode;
12+
const appName = ctx.appName;
1013

1114
const addedNetworks = await createMissingNetworks(ctx);
1215
for (const n of addedNetworks) {
@@ -20,6 +23,30 @@ export async function handler (args: ArgumentsCamelCase) {
2023

2124
await removeUnusedServices(ctx);
2225
await removeUnusedConfigs(ctx);
26+
27+
let services;
28+
let updateStatusesDone = false;
29+
do {
30+
services = await dockerode.listServices({filters: {label: [`com.docker.stack.namespace=${appName}`]}});
31+
updateStatusesDone = services.every(s => {
32+
if (!s.UpdateStatus?.State) return true;
33+
return ["completed", "rollback_completed", "paused", "rollback_paused"].includes(s.UpdateStatus?.State ?? "");
34+
});
35+
36+
// To prevent high cpu usage
37+
await timers.setTimeout(1000);
38+
} while (!updateStatusesDone);
39+
const failed = services.filter(s => {
40+
return ["paused", "rollback_paused", "rollback_completed"].includes(s.UpdateStatus?.State ?? "");
41+
});
42+
if (failed.length > 0) {
43+
for (const s of failed) {
44+
console.error(`${s.Spec?.Name} ${s.UpdateStatus?.Message}`);
45+
}
46+
console.error("Deployment failed");
47+
process.exit(1);
48+
}
49+
console.error("Deployment succeeded");
2350
}
2451

2552
export function builder (yargs: Argv) {

src/commands/wait-cmd.ts

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,84 @@
11
import {ArgumentsCamelCase, Argv} from "yargs";
22
import Docker from "dockerode";
33
import timers from "timers/promises";
4-
import {assertString} from "../asserts.js";
4+
import {assertNumber, assertString} from "../asserts.js";
5+
6+
interface Task {
7+
DesiredState: string;
8+
Status: {
9+
State: string;
10+
Err: string;
11+
};
12+
}
513

614
export const command = "wait <app-name>";
715
export const description = "Wait for deployment to settle";
816

917
export async function handler (args: ArgumentsCamelCase) {
1018
const appName = args["appName"];
1119
assertString(appName);
20+
const timeout = args["timeout"];
21+
assertNumber(timeout);
1222

1323
const dockerode = new Docker();
1424

15-
console.log("Waiting for deployment reconciliation");
25+
console.log(`Awaiting task reconciliation for a max of ${timeout}ms`);
1626

1727
let services;
18-
let reconciled = false;
28+
let timedout = false;
29+
let reconciled;
30+
let latestTaskError = "";
31+
const start = Date.now();
1932
do {
33+
latestTaskError = "";
34+
reconciled = true;
2035
services = await dockerode.listServices({filters: {label: [`com.docker.stack.namespace=${appName}`]}});
21-
reconciled = services.every(s => {
22-
if (!s.UpdateStatus?.State) return true;
23-
return ["completed", "rollback_completed", "paused", "rollback_paused"].includes(s.UpdateStatus?.State ?? "");
24-
});
25-
await timers.setTimeout(1000);
26-
} while (!reconciled);
27-
28-
const paused = services.filter(s => {
29-
return ["paused", "rollback_paused"].includes(s.UpdateStatus?.State ?? "");
30-
});
31-
if (paused.length > 0) {
32-
for (const s of paused) {
33-
console.error(`${s.Spec?.Name} ${s.UpdateStatus?.Message}`);
36+
37+
// Check the tasks for failures.
38+
for (const s of services) {
39+
const tasks = await dockerode.listTasks({
40+
Filter: `service=${s.Spec?.Name}`,
41+
}) as Task[];
42+
for (const t of tasks) {
43+
if (t.DesiredState === "ready" && t.Status.State != "running") {
44+
reconciled = false;
45+
}
46+
if (t.Status.State === "rejected" && latestTaskError == "") {
47+
latestTaskError = t.Status.Err;
48+
}
49+
}
50+
}
51+
52+
// To prevent high cpu usage
53+
await timers.setTimeout(5000);
54+
timedout = Date.now() - timeout > start;
55+
if (!reconciled && latestTaskError != "") {
56+
console.error(latestTaskError);
57+
}
58+
} while (!timedout && !reconciled);
59+
60+
if (!reconciled || timedout) {
61+
if (timedout) {
62+
console.error("Reconciliation timed out");
63+
} else {
64+
console.error("Reconciliation failed");
3465
}
35-
console.error("Reconciliation failed");
36-
} else {
37-
console.log("Reconciliation succeeded");
66+
67+
process.exit(1);
3868
}
69+
console.log("Reconciliation succeeded");
3970
}
4071

4172
export function builder (yargs: Argv) {
4273
yargs.positional("app-name", {
4374
type: "string",
4475
description: "Application name",
4576
});
77+
yargs.positional("timeout", {
78+
type: "number",
79+
description: "Time is ms to wait for reconciliation",
80+
default: 120000,
81+
});
4682
yargs.hide("help");
4783
yargs.hide("version");
4884
return yargs;

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as generateSchemaCommand from "./commands/generate-schema.js";
99

1010
process.on("uncaughtException", (err) => {
1111
if (err instanceof assert.AssertionError) {
12-
console.error(`\x1b[31m${err.stack?.split("\n").slice(0, 6).join("\n")}\x1b[0m`);
12+
console.error(err.message);
1313
} else {
1414
console.error(`\x1b[31m${err.stack?.split("\n").slice(0, 4).join("\n")}\x1b[0m`);
1515
}

0 commit comments

Comments
 (0)