-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathaction.yml
More file actions
138 lines (131 loc) · 6.47 KB
/
Copy pathaction.yml
File metadata and controls
138 lines (131 loc) · 6.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
name: 'Runner Local Cache'
description: >-
Local-disk cache for self-hosted runners — drop-in replacement for actions/cache, less network IO.
author: 'curlewlabs-com'
branding:
icon: 'hard-drive'
color: 'blue'
inputs:
path:
description: 'Path to restore the cached directory to.'
required: true
key:
description: 'Exact cache key to look up.'
required: true
restore-keys:
description: >-
Newline-separated list of key prefixes for fallback matching.
The most recently modified matching entry wins.
required: false
default: ''
cache-dir:
description: >-
Absolute path to local directory where cache entries are stored. Must be
persistent and shared across all runners on the same machine (e.g. a path
outside the per-workspace _work directory). Tilde expansion is not supported.
required: true
outputs:
cache-hit:
description: "'true' if an exact key match was found, 'false' otherwise."
# Phase 1 surfaces it from the target-locked quick check; Phase 2 from the
# restore. Both come via local-mutex's output-file (see the surface steps).
value: ${{ steps.check.outputs.cache-hit || steps.restore.outputs.cache-hit }}
cache-matched-key:
description: >-
The key that was actually restored. Equal to 'key' on an exact hit;
usually the matched raw key on a prefix hit; empty on a miss.
value: ${{ steps.check.outputs.cache-matched-key || steps.restore.outputs.cache-matched-key }}
runs:
using: 'composite'
steps:
# Phase 0: normalize the target path to one canonical spelling so the
# per-target lock name is derived from it. Two spellings of the same
# directory (a trailing slash, //, /./) would otherwise take different
# cache-target-<path> locks and let the gc reclaim a target a concurrent
# restore is using. Only the LOCK NAME is canonicalized here; the restore
# still operates on the caller's own path (same physical dir), and
# cache-restore.sh records the same normalized path for the gc to lock on.
- name: Normalize target path
id: norm
shell: sh
env:
INPUT_PATH: ${{ inputs.path }}
GH_ACTION_PATH: ${{ github.action_path }}
run: |
. "$GH_ACTION_PATH/lib/cache-common.sh"
printf 'path=%s\n' "$(normalize_path "$INPUT_PATH")" >> "$GITHUB_OUTPUT"
# Phase 1: Quick check under the per-TARGET lock.
# If the target already holds the requested key (steady state) we finish
# here without taking the per-key lock. The lock is keyed by the target
# PATH, not the cache key, so distinct runners restoring to their own paths
# never contend — this stays parallel across the fleet. What it does
# serialize against is gc's target reclaim (gc holds the same
# cache-target-<path> lock): is_current and the last-use bump can't
# interleave with a reclaim of that same target. local-mutex captures the
# check's GITHUB_OUTPUT writes in output-file; the next step surfaces them.
- name: Quick check (target-locked)
id: check-lock
uses: curlewlabs-com/local-mutex@88393519d9c8488eeea41bbbb810e33a1ac6609a # v2.1.0
env:
INPUT_PATH: ${{ inputs.path }}
INPUT_KEY: ${{ inputs.key }}
INPUT_CACHE_DIR: ${{ inputs.cache-dir }}
INPUT_RESTORE_KEYS: ${{ inputs.restore-keys }}
GH_ACTION_PATH: ${{ github.action_path }}
with:
name: cache-target-${{ steps.norm.outputs.path }}
run: |
sh "$GH_ACTION_PATH/lib/cache-restore.sh" \
--check \
"$INPUT_PATH" \
"$INPUT_KEY" \
"$INPUT_CACHE_DIR" \
"$INPUT_RESTORE_KEYS"
- name: Surface quick-check outputs
id: check
shell: sh
run: cat "${{ steps.check-lock.outputs.output-file }}" >> "$GITHUB_OUTPUT"
# Phase 2: Serialized restore. Runs when Phase 1 found a match it could not
# serve from an already-current target (skipped on a miss, when matched-key
# is empty). Takes the per-KEY lock on the MATCHED entry's own key — not the
# requested key — so it is the very lock gc takes to evict that entry: on a
# restore-keys (prefix) hit the served entry's stored key differs from the
# requested key, and locking the requested key would leave the rsync SOURCE
# unprotected against a concurrent eviction. Nested inside it is the
# per-TARGET lock (protects the target as the rsync DEST against gc reclaim).
# The nest order is key ⊃ target — the same order gc acquires them — so a
# restore and a sweep can never deadlock. cache-restore.sh restores the exact
# entry Phase 1 resolved (--restore <matched-name>), never re-resolving to a
# different, unlocked one. The inner lock is taken via local-mutex's own CLI
# ($LOCAL_MUTEX_CLI), inheriting its lock domain ($LOCAL_MUTEX_LOCK_DIR), so
# it lands in the same domain as Phase 1 and gc. Items pass to the inner
# command via the environment (INPUT_*/MATCHED_NAME/GH_ACTION_PATH already
# exported here and inherited), never string-interpolated into the command.
#
# local-mutex is pinned to a commit SHA (not a version tag, which stays
# movable) so a local-cache@v3 consumer sees deterministic behavior across
# reruns. See AGENTS.md for the rationale.
- name: Restore (locked)
if: steps.check.outputs.skip-lock != 'true' && steps.check.outputs.matched-key != ''
id: restore-lock
uses: curlewlabs-com/local-mutex@88393519d9c8488eeea41bbbb810e33a1ac6609a # v2.1.0
env:
INPUT_PATH: ${{ inputs.path }}
INPUT_KEY: ${{ inputs.key }}
INPUT_CACHE_DIR: ${{ inputs.cache-dir }}
MATCHED_NAME: ${{ steps.check.outputs.matched-name }}
GH_ACTION_PATH: ${{ github.action_path }}
NORM_PATH: ${{ steps.norm.outputs.path }}
with:
name: cache-save-${{ steps.check.outputs.matched-key }}
run: |
sh "$LOCAL_MUTEX_CLI" "cache-target-$NORM_PATH" \
'sh "$GH_ACTION_PATH/lib/cache-restore.sh" --restore "$MATCHED_NAME" "$INPUT_PATH" "$INPUT_KEY" "$INPUT_CACHE_DIR"'
# Propagate cache-restore.sh outputs across the composite action
# boundary. local-mutex captures them in output-file; we read them
# into this step's GITHUB_OUTPUT so the action-level outputs resolve.
- name: Propagate restore outputs
if: steps.check.outputs.skip-lock != 'true' && steps.check.outputs.matched-key != ''
id: restore
shell: sh
run: cat "${{ steps.restore-lock.outputs.output-file }}" >> "$GITHUB_OUTPUT"