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

AJGranowski / preceding-tag-action / 25259496350

02 May 2026 07:03PM UTC coverage: 93.305% (-0.4%) from 93.711%
25259496350

Pull #77

github

web-flow
Merge d0ec13837 into dfaf7e904
Pull Request #77: Add request cache

172 of 183 branches covered (93.99%)

Branch coverage included in aggregate %.

163 of 176 new or added lines in 4 files covered. (92.61%)

4 existing lines in 2 files now uncovered.

483 of 519 relevant lines covered (93.06%)

12.21 hits per line

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

93.57
/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 type { Octokit } from "@octokit/core";
5
import type { RequestParameters } from "@octokit/types";
6

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

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

15
interface OctokitPluginRequestCacheReturn {
16
    loadCache: (key: string) => Promise<void>;
17
    saveCache: (key: string) => Promise<void>;
18
}
19

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

114
        if (response.headers.etag != null) {
1✔
115
            const requestHash = hashRequestParameters(options);
1✔
116
            optionsWithDefaults.requestCache.put(requestHash, response.headers.etag, response, Date.now());
1✔
117
        }
1✔
118
    });
4✔
119

120
    octokit.hook.error("request", async (error: any) => {
4✔
121
        if (!(await optionsWithDefaults.requestCache.isOpen())) {
2!
NEW
122
            return;
×
NEW
123
        }
×
124

125
        if (error.status === 304 && error.response != null && error.response.headers != null && error.response.headers.etag != null) {
2✔
126
            const cachedResponse = await optionsWithDefaults.requestCache.matchResponse(error.response.headers.etag);
2✔
127
            if (cachedResponse != null) {
2✔
128
                return cachedResponse.response;
1✔
129
            }
1✔
130
        }
2✔
131

132
        throw error;
1✔
133
    });
4✔
134

135
    return {
4✔
136
        async loadCache(key: string): Promise<void> {
4✔
137
            await optionsWithDefaults.actionsCache.restoreCache([pathJoin(CACHE_DIR, "*")], key);
2✔
138
            await optionsWithDefaults.requestCache.open();
2✔
139
        },
2✔
140
        async saveCache(key: string): Promise<void> {
4✔
141
            await optionsWithDefaults.requestCache.close();
2✔
142
            await optionsWithDefaults.actionsCache.saveCache([pathJoin(CACHE_DIR, "*")], key);
2✔
143
        }
2✔
144
    };
4✔
145
}
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