• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

AJGranowski / preceding-tag-action / 25261660619

02 May 2026 08:54PM UTC coverage: 93.454% (-0.3%) from 93.711%
25261660619

push

github

web-flow
Add request cache (#77)

173 of 184 branches covered (94.02%)

Branch coverage included in aggregate %.

178 of 191 new or added lines in 5 files covered. (93.19%)

498 of 534 relevant lines covered (93.26%)

11.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.84
/src/OctokitPluginRequestCache.ts
1
import * as actionsCache from "@actions/cache";
1✔
2
import { hash } from "crypto";
1✔
3
import { join as pathJoin } from "path";
1✔
4
import { mkdir } from "fs/promises";
1✔
5
import type { Octokit } from "@octokit/core";
6
import type { RequestParameters } from "@octokit/types";
7

8
import { ETagRequestCacheDB } from "./ETagRequestCacheDB";
1✔
9

10
interface OctokitPluginRequestCacheOptions {
11
    actionsCache?: typeof actionsCache;
12
    enable?: boolean;
13
    requestCache?: ETagRequestCacheDB
14
}
15

16
interface OctokitPluginRequestCacheReturn {
17
    loadCache: (primaryKey: string, restoreKey: string) => Promise<void>;
18
    saveCache: (primaryKey: string) => Promise<void>;
19
}
20

21
const CACHE_DIR = pathJoin("/", "tmp", ".cache", "preceding-tag-action");
1✔
22

23
function mapObject(obj: Record<string, unknown>, callback: (key: string, value: unknown) => unknown): Record<string, unknown> {
9✔
24
    return Object.entries(obj).reduce((result, [key, value]: [string, any]) => {
9✔
25
        const mappedValue = callback(key, value);
24✔
26
        if (mappedValue !== undefined) {
24✔
27
            result[key] = mappedValue;
15✔
28
        }
15✔
29

30
        return result;
24✔
31
    }, {} as Record<string, unknown>);
9✔
32
}
9✔
33

34
function hashRequestParameters(requestParameters: RequestParameters): string {
3✔
35
    const headerMapper = (key: string, value: unknown): unknown => {
3✔
36
        if (key !== "accept") {
6✔
37
            return undefined;
3✔
38
        }
3✔
39

40
        return value;
3✔
41
    };
6✔
42

43
    const mediaTypeMapper = (key: string, value: unknown): unknown => {
3✔
44
        if (key !== "format") {
6✔
45
            return undefined;
3✔
46
        }
3✔
47

48
        return value;
3✔
49
    };
6✔
50

51
    const objectToHash = mapObject(requestParameters, (key, value) => {
3✔
52
        if (key === "request") {
12✔
53
            return undefined;
3✔
54
        }
3✔
55

56
        if (key === "headers") {
12✔
57
            return mapObject(value as any, headerMapper);
3✔
58
        }
3✔
59

60
        if (key === "mediaType") {
12✔
61
            return mapObject(value as any, mediaTypeMapper);
3✔
62
        }
3✔
63

64
        return value;
3✔
65
    });
3✔
66

67
    return hash("sha256", JSON.stringify(objectToHash));
3✔
68
}
3✔
69

70
export function requestCache(octokit: Octokit, options: OctokitPluginRequestCacheOptions | any): OctokitPluginRequestCacheReturn {
1✔
71
    const optionsWithDefaults = {
5✔
72
        actionsCache: actionsCache,
5✔
73
        enable: true,
5✔
74
        requestCache: new ETagRequestCacheDB(),
5✔
75
        ...options as OctokitPluginRequestCacheOptions
5✔
76
    } satisfies Required<OctokitPluginRequestCacheOptions>;
5✔
77

78
    // Early return if disabled
79
    if (!optionsWithDefaults.enable) {
5✔
80
        return {
1✔
81
            loadCache: (): Promise<void> => Promise.resolve(),
1✔
82
            saveCache: (): Promise<void> => Promise.resolve()
1✔
83
        };
1✔
84
    }
1✔
85

86
    octokit.hook.before("request", async (options) => {
4✔
87
        if (!(await optionsWithDefaults.requestCache.isOpen())) {
4!
NEW
88
            return;
×
NEW
89
        }
×
90

91
        let cacheControl = options.headers["cache-control"];
4✔
92
        cacheControl = cacheControl == null ? "" : cacheControl.toString();
4✔
93
        if (cacheControl.includes("no-cache")) {
4✔
94
            return;
2✔
95
        }
2✔
96

97
        const requestHash = hashRequestParameters(options);
2✔
98
        const eTag = await optionsWithDefaults.requestCache.matchETag(requestHash);
2✔
99
        if (eTag != null) {
4✔
100
            options.headers["If-None-Match"] = eTag;
1✔
101
        }
1✔
102
    });
4✔
103

104
    octokit.hook.after("request", async (response, options) => {
4✔
105
        if (!(await optionsWithDefaults.requestCache.isOpen())) {
2!
NEW
106
            return;
×
NEW
107
        }
×
108

109
        let cacheControl = options.headers["cache-control"];
2✔
110
        cacheControl = cacheControl == null ? "" : cacheControl.toString();
2✔
111
        if (cacheControl.includes("no-store")) {
2✔
112
            return;
1✔
113
        }
1✔
114

115
        if (response.headers.etag != null) {
1✔
116
            const etagMatcher = response.headers.etag.match(/("[^"]+")/);
1✔
117
            if (etagMatcher != null && etagMatcher[1] != null) {
1✔
118
                const etag = etagMatcher[1];
1✔
119
                const requestHash = hashRequestParameters(options);
1✔
120
                optionsWithDefaults.requestCache.put(requestHash, etag, response, Date.now());
1✔
121
            }
1✔
122
        }
1✔
123
    });
4✔
124

125
    octokit.hook.error("request", async (error: any) => {
4✔
126
        if (!(await optionsWithDefaults.requestCache.isOpen())) {
2!
NEW
127
            return;
×
NEW
128
        }
×
129

130
        if (error.status === 304 && error.response != null && error.response.headers != null && error.response.headers.etag != null) {
2✔
131
            const cachedResponse = await optionsWithDefaults.requestCache.matchResponse(error.response.headers.etag);
2✔
132
            if (cachedResponse != null) {
2✔
133
                return cachedResponse.response;
1✔
134
            }
1✔
135
        }
2✔
136

137
        throw error;
1✔
138
    });
4✔
139

140
    return {
4✔
141
        async loadCache(primaryKey: string, restoreKey: string): Promise<void> {
4✔
142
            await mkdir(CACHE_DIR, {recursive: true});
2✔
143
            await optionsWithDefaults.actionsCache.restoreCache([pathJoin(CACHE_DIR, "*")], primaryKey, [restoreKey]);
2✔
144
            await optionsWithDefaults.requestCache.open();
2✔
145
        },
2✔
146
        async saveCache(primaryKey: string): Promise<void> {
4✔
147
            await optionsWithDefaults.requestCache.close();
2✔
148
            await optionsWithDefaults.actionsCache.saveCache([pathJoin(CACHE_DIR, "*")], primaryKey);
2✔
149
        }
2✔
150
    };
4✔
151
}
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc