Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardize to handle durations as Temporal.Duration in every inner interfaces #831

Merged
merged 8 commits into from
Jun 3, 2024
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: 2 additions & 2 deletions .github/workflows/GH-820-graceperiod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
- uses: ./
with:
retry-method: 'equal_intervals'
wait-seconds-before-first-polling: '0'
wait-seconds-before-first-polling: '1'
min-interval-seconds: '5'
attempt-limits: '100'
wait-list: |
Expand All @@ -77,7 +77,7 @@ jobs:
- uses: ./
with:
retry-method: 'equal_intervals'
wait-seconds-before-first-polling: '0'
wait-seconds-before-first-polling: '1'
min-interval-seconds: '5'
attempt-limits: '100'
wait-list: |
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This file only records notable changes. Not synchronized with all releases and t

- main - not yet released
- Add `startupGracePeriod` option in wait-list: [#820](https://github.com/kachick/wait-other-jobs/issues/820)
- Restrict `wait-seconds-before-first-polling` if it is too short as zero or shorter than `startupGracePeriod`
- v3.2.0
- Add `eventName` option in wait-list: [#771](https://github.com/kachick/wait-other-jobs/issues/771)
- v3.1.0
Expand Down
78 changes: 44 additions & 34 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31042,6 +31042,12 @@ var MyDurationLike = z2.object({
nanoseconds: z2.number().optional()
}).strict().readonly();
var Durationable = z2.union([z2.string().duration(), MyDurationLike]).transform((item) => getDuration(item));
var Duration = z2.instanceof(mr.Duration).refine(
(d2) => mr.Duration.compare(d2, { seconds: 0 }) > 0,
{
message: "Too short interval for pollings"
}
);
var defaultGrace = mr.Duration.from({ seconds: 10 });
function isDurationLike(my) {
for (const [_2, value] of Object.entries(my)) {
Expand All @@ -31053,7 +31059,7 @@ function isDurationLike(my) {
}
function getDuration(durationable) {
if (typeof durationable === "string" || isDurationLike(durationable)) {
return mr.Duration.from(durationable);
return Duration.parse(mr.Duration.from(durationable));
}
throw new Error("unexpected value is specified in durations");
}
Expand All @@ -31079,26 +31085,26 @@ var retryMethods = z2.enum(["exponential_backoff", "equal_intervals"]);
var Options = z2.object({
waitList: WaitList,
skipList: SkipList,
waitSecondsBeforeFirstPolling: z2.number().min(0),
minIntervalSeconds: z2.number().min(1),
initialDuration: Duration,
leastInterval: Duration,
retryMethod: retryMethods,
attemptLimits: z2.number().min(1),
isEarlyExit: z2.boolean(),
shouldSkipSameWorkflow: z2.boolean(),
isDryRun: z2.boolean()
}).readonly().refine(
}).strict().readonly().refine(
({ waitList, skipList }) => !(waitList.length > 0 && skipList.length > 0),
{ message: "Do not specify both wait-list and skip-list", path: ["waitList", "skipList"] }
).refine(
({ waitSecondsBeforeFirstPolling, waitList }) => waitList.every(
({ initialDuration, waitList }) => waitList.every(
(item) => !(mr.Duration.compare(
{ seconds: waitSecondsBeforeFirstPolling },
initialDuration,
item.startupGracePeriod
) > 0 && mr.Duration.compare(item.startupGracePeriod, defaultGrace) !== 0)
),
{
message: "A shorter startupGracePeriod waiting for the first poll does not make sense",
path: ["waitSecondsBeforeFirstPolling", "waitList"]
path: ["initialDuration", "waitList"]
}
);

Expand Down Expand Up @@ -31142,8 +31148,8 @@ function parseInput() {
const shouldSkipSameWorkflow = (0, import_core.getBooleanInput)("skip-same-workflow", { required: true, trimWhitespace: true });
const isDryRun = (0, import_core.getBooleanInput)("dry-run", { required: true, trimWhitespace: true });
const options = Options.parse({
waitSecondsBeforeFirstPolling,
minIntervalSeconds,
initialDuration: Durationable.parse({ seconds: waitSecondsBeforeFirstPolling }),
leastInterval: Durationable.parse({ seconds: minIntervalSeconds }),
retryMethod,
attemptLimits,
waitList: JSON.parse((0, import_core.getInput)("wait-list", { required: true })),
Expand Down Expand Up @@ -32360,6 +32366,17 @@ function groupBy(items, callback) {
}

// src/report.ts
function readableDuration(duration) {
const { hours, minutes, seconds } = duration.round({ largestUnit: "hours" });
const eachUnit = [`${seconds} seconds`];
if (minutes > 0) {
eachUnit.unshift(`${minutes} minutes`);
}
if (hours > 0) {
eachUnit.unshift(`${hours} hours`);
}
return `about ${eachUnit.join(" ")}`;
}
function summarize(check, trigger) {
const { checkRun: run2, checkSuite: suite, workflow, workflowRun } = check;
const isCompleted = run2.status === "COMPLETED";
Expand Down Expand Up @@ -32482,40 +32499,34 @@ function generateReport(summaries, trigger, elapsed, { waitList, skipList, shoul

// src/wait.ts
import { setTimeout as setTimeout2 } from "timers/promises";
var wait = setTimeout2;
var waitPrimitive = setTimeout2;
function wait(interval) {
return waitPrimitive(interval.total("milliseconds"));
}
function getRandomInt(min, max) {
const flooredMin = Math.ceil(min);
return Math.floor(Math.random() * (Math.floor(max) - flooredMin) + flooredMin);
}
function readableDuration(milliseconds) {
const msecToSec = 1e3;
const secToMin = 60;
const seconds = milliseconds / msecToSec;
const minutes = seconds / secToMin;
const { unit, value, precision } = minutes >= 1 ? { unit: "minutes", value: minutes, precision: 1 } : { unit: "seconds", value: seconds, precision: 0 };
const adjustor = 10 ** precision;
return `about ${(Math.round(value * adjustor) / adjustor).toFixed(
precision
)} ${unit}`;
}
var MIN_JITTER_MILLISECONDS = 1e3;
var MAX_JITTER_MILLISECONDS = 7e3;
function calcExponentialBackoffAndJitter(minIntervalSeconds, attempts) {
function calcExponentialBackoffAndJitter(leastInterval, attempts) {
const jitterMilliseconds = getRandomInt(MIN_JITTER_MILLISECONDS, MAX_JITTER_MILLISECONDS);
return minIntervalSeconds * 2 ** (attempts - 1) * 1e3 + jitterMilliseconds;
return mr.Duration.from({
milliseconds: leastInterval.total("milliseconds") * 2 ** (attempts - 1) + jitterMilliseconds
});
}
function getIdleMilliseconds(method, minIntervalSeconds, attempts) {
function getInterval(method, leastInterval, attempts) {
switch (method) {
case "exponential_backoff":
return calcExponentialBackoffAndJitter(
minIntervalSeconds,
leastInterval,
attempts
);
case "equal_intervals":
return minIntervalSeconds * 1e3;
return leastInterval;
default: {
const _exhaustiveCheck = method;
return minIntervalSeconds * 1e3;
return leastInterval;
}
}
}
Expand Down Expand Up @@ -32568,16 +32579,15 @@ async function run() {
break;
}
if (attempts === 1) {
const initialMsec = options.waitSecondsBeforeFirstPolling * 1e3;
(0, import_core3.info)(`Wait ${readableDuration(initialMsec)} before first polling.`);
await wait(initialMsec);
(0, import_core3.info)(`Wait ${readableDuration(options.initialDuration)} before first polling.`);
await wait(options.initialDuration);
} else {
const msec = getIdleMilliseconds(options.retryMethod, options.minIntervalSeconds, attempts);
(0, import_core3.info)(`Wait ${readableDuration(msec)} before next polling to reduce API calls.`);
await wait(msec);
const interval = getInterval(options.retryMethod, options.leastInterval, attempts);
(0, import_core3.info)(`Wait ${readableDuration(interval)} before next polling to reduce API calls.`);
await wait(interval);
}
const elapsed = mr.Duration.from({ milliseconds: Math.ceil(performance.now() - startedAt) });
(0, import_core3.startGroup)(`Polling ${attempts}: ${(/* @__PURE__ */ new Date()).toISOString()}(${elapsed.toString()}) ~`);
(0, import_core3.startGroup)(`Polling ${attempts}: ${(/* @__PURE__ */ new Date()).toISOString()} # total elapsed ${readableDuration(elapsed)})`);
const checks = await fetchChecks(githubToken, trigger);
if ((0, import_core3.isDebug)()) {
(0, import_core3.debug)(JSON.stringify({ label: "rawdata", checks, elapsed }, null, 2));
Expand Down
6 changes: 3 additions & 3 deletions src/input.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { debug, getInput, getBooleanInput, setSecret, isDebug, error } from '@actions/core';
import { context } from '@actions/github';

import { Options, Trigger } from './schema.ts';
import { Durationable, Options, Trigger } from './schema.ts';

export function parseInput(): { trigger: Trigger; options: Options; githubToken: string } {
const {
Expand Down Expand Up @@ -45,8 +45,8 @@ export function parseInput(): { trigger: Trigger; options: Options; githubToken:
const isDryRun = getBooleanInput('dry-run', { required: true, trimWhitespace: true });

const options = Options.parse({
waitSecondsBeforeFirstPolling,
minIntervalSeconds,
initialDuration: Durationable.parse({ seconds: waitSecondsBeforeFirstPolling }),
leastInterval: Durationable.parse({ seconds: minIntervalSeconds }),
retryMethod,
attemptLimits,
waitList: JSON.parse(getInput('wait-list', { required: true })),
Expand Down
17 changes: 8 additions & 9 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ function colorize(severity: Severity, message: string): string {

import { parseInput } from './input.ts';
import { fetchChecks } from './github-api.ts';
import { Severity, generateReport, getSummaries } from './report.ts';
import { readableDuration, wait, getIdleMilliseconds } from './wait.ts';
import { Severity, generateReport, getSummaries, readableDuration } from './report.ts';
import { getInterval, wait } from './wait.ts';
import { Temporal } from 'temporal-polyfill';

async function run(): Promise<void> {
Expand Down Expand Up @@ -58,18 +58,17 @@ async function run(): Promise<void> {
}

if (attempts === 1) {
const initialMsec = options.waitSecondsBeforeFirstPolling * 1000;
info(`Wait ${readableDuration(initialMsec)} before first polling.`);
await wait(initialMsec);
info(`Wait ${readableDuration(options.initialDuration)} before first polling.`);
await wait(options.initialDuration);
} else {
const msec = getIdleMilliseconds(options.retryMethod, options.minIntervalSeconds, attempts);
info(`Wait ${readableDuration(msec)} before next polling to reduce API calls.`);
await wait(msec);
const interval = getInterval(options.retryMethod, options.leastInterval, attempts);
info(`Wait ${readableDuration(interval)} before next polling to reduce API calls.`);
await wait(interval);
}

// Put getting elapsed time before of fetchChecks to keep accuracy of the purpose
const elapsed = Temporal.Duration.from({ milliseconds: Math.ceil(performance.now() - startedAt) });
startGroup(`Polling ${attempts}: ${(new Date()).toISOString()}(${elapsed.toString()}) ~`);
startGroup(`Polling ${attempts}: ${(new Date()).toISOString()} # total elapsed ${readableDuration(elapsed)})`);
const checks = await fetchChecks(githubToken, trigger);
if (isDebug()) {
debug(JSON.stringify({ label: 'rawdata', checks, elapsed }, null, 2));
Expand Down
11 changes: 10 additions & 1 deletion src/report.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import test from 'node:test';
import assert from 'node:assert';
import { checks8679817057, checks92810686811WaitSuccessPolling1 } from './snapshot.ts';
import { Report, Summary, generateReport, getSummaries } from './report.ts';
import { Report, Summary, generateReport, getSummaries, readableDuration } from './report.ts';
import { omit } from './util.ts';
import { Temporal } from 'temporal-polyfill';
import { jsonEqual } from './assert.ts';

test('readableDuration', () => {
assert.strictEqual(readableDuration(Temporal.Duration.from({ milliseconds: 454356 })), 'about 7 minutes 34 seconds');
assert.strictEqual(readableDuration(Temporal.Duration.from({ milliseconds: 32100 })), 'about 32 seconds');
assert.strictEqual(
readableDuration(Temporal.Duration.from({ hours: 4, minutes: 100, seconds: 79 })),
'about 5 hours 41 minutes 19 seconds',
);
});

const exampleSummary = Object.freeze(
{
isAcceptable: false,
Expand Down
12 changes: 12 additions & 0 deletions src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import { join, relative } from 'path';
import { Temporal } from 'temporal-polyfill';
import { groupBy } from './util.ts';

export function readableDuration(duration: Temporal.Duration): string {
const { hours, minutes, seconds } = duration.round({ largestUnit: 'hours' });
const eachUnit = [`${seconds} seconds`];
if (minutes > 0) {
eachUnit.unshift(`${minutes} minutes`);
}
if (hours > 0) {
eachUnit.unshift(`${hours} hours`);
}
return `about ${eachUnit.join(' ')}`;
}

export interface Summary {
isAcceptable: boolean;
isCompleted: boolean;
Expand Down
26 changes: 13 additions & 13 deletions src/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'node:test';
import { deepStrictEqual, throws } from 'node:assert';
import { throws } from 'node:assert';
import { Durationable, Options } from './schema.ts';
import { Temporal } from 'temporal-polyfill';
import { durationEqual, optionsEqual } from './assert.ts';
Expand All @@ -9,21 +9,21 @@ const defaultOptions = Object.freeze({
attemptLimits: 1000,
waitList: [],
skipList: [],
waitSecondsBeforeFirstPolling: 10,
minIntervalSeconds: 15,
initialDuration: Temporal.Duration.from({ seconds: 10 }),
leastInterval: Temporal.Duration.from({ seconds: 15 }),
retryMethod: 'equal_intervals',
shouldSkipSameWorkflow: false,
isDryRun: false,
});

test('Options keep given values', () => {
deepStrictEqual({
optionsEqual({
isEarlyExit: true,
attemptLimits: 1000,
waitList: [],
skipList: [],
waitSecondsBeforeFirstPolling: 10,
minIntervalSeconds: 15,
initialDuration: Temporal.Duration.from({ seconds: 10 }),
leastInterval: Temporal.Duration.from({ seconds: 15 }),
retryMethod: 'equal_intervals',
shouldSkipSameWorkflow: false,
isDryRun: false,
Expand All @@ -45,9 +45,9 @@ test('Options set some default values it cannot be defined in action.yml', () =>
});

test('Options reject invalid values', () => {
throws(() => Options.parse({ ...defaultOptions, minIntervalSeconds: 0 }), {
throws(() => Options.parse({ ...defaultOptions, leastInterval: Temporal.Duration.from({ seconds: 0 }) }), {
name: 'ZodError',
message: /too_small/,
message: /Too short interval for pollings/,
});

throws(() => Options.parse({ ...defaultOptions, attemptLimits: 0 }), {
Expand Down Expand Up @@ -182,7 +182,7 @@ test('wait-list have startupGracePeriod', async (t) => {
() =>
Options.parse({
...defaultOptions,
waitSecondsBeforeFirstPolling: 41,
initialDuration: Temporal.Duration.from({ seconds: 41 }),
waitList: [{ workflowFile: 'ci.yml', startupGracePeriod: { seconds: 40 } }],
}),
{
Expand All @@ -196,12 +196,12 @@ test('wait-list have startupGracePeriod', async (t) => {
optionsEqual(
Options.parse({
...defaultOptions,
waitSecondsBeforeFirstPolling: 42,
initialDuration: Temporal.Duration.from({ seconds: 42 }),
waitList: [{ workflowFile: 'ci.yml', startupGracePeriod: { seconds: 10 } }],
}),
{
...defaultOptions,
waitSecondsBeforeFirstPolling: 42,
initialDuration: Temporal.Duration.from({ seconds: 42 }),
waitList: [{
workflowFile: 'ci.yml',
optional: false,
Expand All @@ -213,12 +213,12 @@ test('wait-list have startupGracePeriod', async (t) => {
optionsEqual(
Options.parse({
...defaultOptions,
waitSecondsBeforeFirstPolling: 42,
initialDuration: Temporal.Duration.from({ seconds: 42 }),
waitList: [{ workflowFile: 'ci.yml' }],
}),
{
...defaultOptions,
waitSecondsBeforeFirstPolling: 42,
initialDuration: Temporal.Duration.from({ seconds: 42 }),
waitList: [{
workflowFile: 'ci.yml',
optional: false,
Expand Down
Loading
Loading