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

AJGranowski / preceding-tag-action / 25699778044

11 May 2026 09:58PM UTC coverage: 92.511% (-0.1%) from 92.634%
25699778044

push

github

web-flow
Fix ETag cache storage bug (#94)

158 of 175 branches covered (90.29%)

Branch coverage included in aggregate %.

20 of 22 new or added lines in 2 files covered. (90.91%)

1 existing line in 1 file now uncovered.

262 of 279 relevant lines covered (93.91%)

11.48 hits per line

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

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

8
import { ETagRequestCacheDB } from "./ETagRequestCacheDB";
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");
2✔
22

23
function mapObject(obj: Record<string, unknown>, callback: (key: string, value: unknown) => unknown): Record<string, unknown> {
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
        }
29

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

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

40
        return value;
3✔
41
    };
42

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

48
        return value;
3✔
49
    };
50

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

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

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

64
        return value;
3✔
65
    });
66

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

70
function isNotModified(error: any): boolean {
71
    return error.status === 304 && error.response != null && error.response.headers != null && error.response.headers.etag != null;
2✔
72
}
73

74
function isSuccessResponse(response: OctokitResponse<any, any>): boolean {
75
    return response.status >= 200 && response.status < 300;
1✔
76
}
77

78
export function requestCache(octokit: Octokit, options: OctokitPluginRequestCacheOptions | any): OctokitPluginRequestCacheReturn {
79
    const optionsWithDefaults = {
5✔
80
        actionsCache: actionsCache,
81
        enable: true,
82
        requestCache: new ETagRequestCacheDB(),
83
        ...options as OctokitPluginRequestCacheOptions
84
    } satisfies Required<OctokitPluginRequestCacheOptions>;
85

86
    // Early return if disabled
87
    if (!optionsWithDefaults.enable) {
5✔
88
        return {
1✔
89
            loadCache: (): Promise<void> => Promise.resolve(),
×
90
            saveCache: (): Promise<void> => Promise.resolve()
×
91
        };
92
    }
93

94
    octokit.hook.before("request", async (options) => {
4✔
95
        let cacheControl = options.headers["cache-control"];
4✔
96
        cacheControl = cacheControl == null ? "" : cacheControl.toString();
4✔
97
        if (!(await optionsWithDefaults.requestCache.isOpen()) || cacheControl.includes("no-cache")) {
4✔
98
            return;
2✔
99
        }
100

101
        const requestHash = hashRequestParameters(options);
2✔
102
        const eTag = await optionsWithDefaults.requestCache.matchETag(requestHash);
2✔
103
        if (eTag != null) {
2✔
104
            options.headers["If-None-Match"] = eTag;
1✔
105
        }
106
    });
107

108
    octokit.hook.after("request", async (response, options) => {
4✔
109
        let cacheControl = options.headers["cache-control"];
2✔
110
        cacheControl = cacheControl == null ? "" : cacheControl.toString();
2✔
111
        if (!(await optionsWithDefaults.requestCache.isOpen()) || cacheControl.includes("no-store")) {
2✔
112
            return;
1✔
113
        }
114

115
        if (!isSuccessResponse(response)) {
1!
UNCOV
116
            return;
×
117
        }
118

119
        if (response.headers.etag == null || response.headers.etag.length === 0) {
1!
NEW
120
            return;
×
121
        }
122

123
        optionsWithDefaults.requestCache.put(hashRequestParameters(options), response.headers.etag, response, Date.now());
1✔
124
    });
125

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

131
        if (!isNotModified(error)) {
2!
NEW
132
            throw error;
×
133
        }
134

135
        const cachedResponse = await optionsWithDefaults.requestCache.matchResponse(error.response.headers.etag);
2✔
136
        if (cachedResponse == null) {
2✔
137
            throw new AggregateError([error], "Cache miss");
1✔
138
        }
139

140
        return cachedResponse.response;
1✔
141
    });
142

143
    return {
4✔
144
        async loadCache(primaryKey: string, restoreKey: string): Promise<void> {
145
            await mkdir(CACHE_DIR, {recursive: true});
2✔
146
            if (optionsWithDefaults.actionsCache.isFeatureAvailable()) {
2!
147
                await optionsWithDefaults.actionsCache.restoreCache([pathJoin(CACHE_DIR, "*")], primaryKey, [restoreKey]);
2✔
148
            }
149

150
            await optionsWithDefaults.requestCache.open();
2✔
151
        },
152
        async saveCache(primaryKey: string): Promise<void> {
153
            await optionsWithDefaults.requestCache.close();
2✔
154
            if (optionsWithDefaults.actionsCache.isFeatureAvailable()) {
2!
155
                await optionsWithDefaults.actionsCache.saveCache([pathJoin(CACHE_DIR, "*")], primaryKey);
2✔
156
            }
157
        }
158
    };
159
}
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