Skip to content

Commit 9e71e5e

Browse files
chagongCopilot
andcommitted
Reuse pinned IssueLens team-memory dispatcher
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 09e83d7 commit 9e71e5e

7 files changed

Lines changed: 126 additions & 172 deletions

File tree

‎.github/actions/queue-team-memory/README.md‎

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ The [push caller](../../workflows/team-memory-post-merge.yml) only preserves
55
source identities and dispatches the [coordinator](../../workflows/team-memory-coordinator.yml).
66
Manual reconciliation now runs through the coordinator too, not the old push
77
workflow. No source caller logs in to Azure or invokes the agent.
8+
The caller reuses `microsoft/IssueLens/.github/actions/queue-team-memory`;
9+
this directory contains only Java coordinator validation, setup guidance, and
10+
contract tests, not a second dispatch action.
11+
Its prerequisite is [microsoft/IssueLens#51](https://github.com/microsoft/IssueLens/pull/51),
12+
pinned at `521b1ff80a70140e286ced7500d0b150cf9e6d6c`. Review and merge that
13+
prerequisite first; these repository changes need no hosted runtime deployment.
814

915
Both workflows require **`ISSUELENS_TEAM_MEMORY_COORDINATOR_ENABLED=true`**.
1016
This is a new, disabled-by-default repository variable: the existing
@@ -33,14 +39,14 @@ public visibility, and default branches. These sources currently configure
3339
The two Spring repositories are not included: they currently have no
3440
`.github/issuelens.yml` policy. An allowlist entry is not a source migration or
3541
authorization to ignore policy. Only Java Pack's caller changes in this PR.
36-
Other repositories must adopt this action in separate reviewed PRs.
42+
Other repositories must adopt the shared IssueLens action in separate reviewed PRs.
3743
Renames, transfers, branch changes, or private sources require another review.
3844

3945
The hosted agent must resolve each source's policy and privacy constraints,
4046
verify that its validated destination is this wiki, and stop rather than
4147
override a conflicting policy. Requests explicitly require this destination;
4248
the pinned client's final receipt validator rejects any other wiki. Neither
43-
the dispatch action nor its allowlist grants wiki write access.
49+
the shared dispatch action nor the coordinator's allowlist grants wiki write access.
4450

4551
## Future external callers
4652

@@ -51,16 +57,23 @@ needed for a remote reference:
5157

5258
```yaml
5359
- name: Queue Java team-memory update
54-
uses: microsoft/vscode-java-pack/.github/actions/queue-team-memory@FULL_REVIEWED_COMMIT_SHA
60+
uses: microsoft/IssueLens/.github/actions/queue-team-memory@521b1ff80a70140e286ced7500d0b150cf9e6d6c
5561
with:
62+
coordinator-repository: microsoft/vscode-java-pack
63+
coordinator-workflow: team-memory-coordinator.yml
64+
coordinator-ref: main
5665
source-token: ${{ github.token }}
5766
dispatch-token: ${{ steps.dispatch-token.outputs.token }}
5867
```
5968
60-
Replace the placeholder with the reviewed, merged 40-character revision of
61-
this action; do not use a moving branch. The caller must obtain
69+
The shared IssueLens action is pinned to the immutable prerequisite revision.
70+
Do not use a moving branch. All three coordinator inputs must be supplied
71+
together. `coordinator-ref: main` is the destination
72+
branch, independently of a source default branch such as `develop`.
73+
The caller must obtain
6274
`steps.dispatch-token.outputs.token` using a **dedicated dispatch GitHub App**,
63-
restricted to Java Pack with **Actions write**. The read and dispatch tokens
75+
restricted to Java Pack with **Actions write and Contents read** (the latter
76+
validates the target branch). The read and dispatch tokens
6477
are separate inputs. An explicitly empty token fails; it never falls back.
6578
Do not distribute the hosted IssueLens App's private key to source workflows.
6679
A source repository's `GITHUB_TOKEN` remains repository-scoped regardless of
@@ -107,16 +120,20 @@ default-branch push from the exact source workflow. Its artifact contains
107120
repository/run/workflow identities, before/after SHAs, and commit IDs only:
108121
no commit messages, bodies, code, credentials, or agent output. It is capped
109122
at 64 KiB and 1,000 commits and retained for seven days.
123+
Before its single POST, the shared action revalidates the source and uses the
124+
dispatch token to validate the target repository, exact active workflow path,
125+
and independently selected target branch/full SHA.
110126

111127
Before downloading, the coordinator authenticates source run/attempt and
112128
artifact metadata, repository IDs, workflow, branch, actors, digest, expiry,
113129
and size. Pinned artifact download enforces the digest. Preflight repeats the
114130
metadata checks, validates the downloaded identity-only schema, and reuses
115131
IssueLens's complete bounded push/PR discovery and merge revalidation. The
116-
client is pinned to the actual merged revision of
117-
[microsoft/IssueLens#50](https://github.com/microsoft/IssueLens/pull/50),
118-
`4175ea71e170938826fb847e6bd5108f0f5597cf`. Only the small Java provenance
119-
adapter is local; request, stream, discovery, and receipt machinery is not
132+
client and dispatch action are pinned to the same shared-dispatcher prerequisite
133+
revision, `521b1ff80a70140e286ced7500d0b150cf9e6d6c`, built on the merged
134+
[microsoft/IssueLens#50](https://github.com/microsoft/IssueLens/pull/50).
135+
Only the small Java provenance adapter is local; request, stream, discovery,
136+
and receipt machinery is not
120137
copied or monkey-patched. The pilot's IssueLens-only coordinator adapter is
121138
deliberately not called.
122139

@@ -142,5 +159,7 @@ With the pinned IssueLens action files available locally, set
142159
python -m unittest discover -s .github/actions/queue-team-memory/tests
143160
```
144161

145-
The focused CI workflow loads the same immutable client revision. These checks
146-
use fake API/stream responses and make no dispatch, Azure, or wiki calls.
162+
The focused CI workflow loads the same immutable client and shared-action
163+
revision. These checks pass the shared dispatcher's actual snapshot through
164+
Java preflight and use fake API/stream responses; they make no dispatch, Azure,
165+
or wiki calls.

‎.github/actions/queue-team-memory/action.yml‎

Lines changed: 0 additions & 46 deletions
This file was deleted.

‎.github/actions/queue-team-memory/team_memory.py‎

Lines changed: 7 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Identity-only Java dispatch and cross-repository adapter for the pinned IssueLens client."""
1+
"""Java coordinator provenance adapter for the pinned IssueLens client."""
22

33
import argparse
44
import importlib.util
@@ -15,7 +15,6 @@
1515
COORDINATOR_WORKFLOW = ".github/workflows/team-memory-coordinator.yml"
1616
SOURCE_INPUTS = ("SOURCE_RUN_ID", "SOURCE_RUN_ATTEMPT", "SOURCE_ARTIFACT_ID")
1717
MAX_SOURCE_BYTES = 64 * 1024
18-
MAX_PUSH_COMMITS = 1000
1918
# Reviewed public sources with an existing policy targeting the Java Pack wiki.
2019
SOURCES = {
2120
COORDINATOR: (104967329, "main"),
@@ -72,18 +71,14 @@ def redirect_request(self, *args, **kwargs):
7271
return None
7372

7473

75-
def github_request(path, token, payload=None):
74+
def github_read(path, token):
7675
require(isinstance(token, str) and token.strip(), "Required GitHub token is empty")
77-
return urllib.request.Request(
76+
request = urllib.request.Request(
7877
"https://api.github.com" + path,
79-
data=None if payload is None else json.dumps(payload).encode("utf-8"),
8078
headers={"Authorization": "Bearer " + token, "Accept": "application/vnd.github+json",
8179
"Content-Type": "application/json", "X-GitHub-Api-Version": "2022-11-28"},
8280
)
83-
84-
85-
def github_read(path, token):
86-
with urllib.request.build_opener(NoRedirect()).open(github_request(path, token), timeout=30) as response:
81+
with urllib.request.build_opener(NoRedirect()).open(request, timeout=30) as response:
8782
content = response.read(4 * 1024 * 1024 + 1)
8883
require(len(content) <= 4 * 1024 * 1024, "GitHub response exceeds its size limit")
8984
result = json.loads(content, object_pairs_hook=unique_object)
@@ -118,22 +113,6 @@ def workflow_context(repository, workflow, event_name, token):
118113
return current, event
119114

120115

121-
def validate_push(event, reference, head):
122-
require(all(event.get(flag) is False for flag in ("created", "deleted", "forced")),
123-
"Created, deleted, or forced refs require manual PR reconciliation")
124-
before, after = full_sha(event.get("before")), full_sha(event.get("after"))
125-
require(before != after and event.get("ref") == reference and head == after
126-
and isinstance(event.get("head_commit"), dict) and event["head_commit"].get("id") == after,
127-
"Push source identity mismatch")
128-
commits = event.get("commits")
129-
require(isinstance(commits, list) and 0 < len(commits) <= MAX_PUSH_COMMITS
130-
and all(isinstance(item, dict) for item in commits), "Missing or oversized push inventory")
131-
shas = [full_sha(item.get("id")) for item in commits]
132-
require(len(set(shas)) == len(shas) and after in shas and before not in shas,
133-
"Push inventory has duplicate or mismatched identities")
134-
return before, after, shas
135-
136-
137116
def source_event_path():
138117
return Path(os.environ["RUNNER_TEMP"]) / "issuelens-team-memory-source" / "source-event.json"
139118

@@ -144,62 +123,6 @@ def outputs(values):
144123
output.write(f"{name}={value}\n")
145124

146125

147-
def push_snapshot():
148-
repository = os.environ["GITHUB_REPOSITORY"]
149-
current, event = workflow_context(repository, SOURCE_WORKFLOW, "push", os.environ["GH_TOKEN"])
150-
before, after, shas = validate_push(event, os.environ["GITHUB_REF"], os.environ["GITHUB_SHA"])
151-
require(os.environ["GITHUB_WORKFLOW_SHA"] == after, "Source workflow revision does not match the push")
152-
actors = [os.environ[name] for name in ("GITHUB_ACTOR", "GITHUB_TRIGGERING_ACTOR")]
153-
require(all(re.fullmatch(r"[A-Za-z0-9-]+(?:\[bot\])?", actor) for actor in actors), "Invalid source actors")
154-
return {
155-
"metadata": {
156-
"repository": repository, "repository_id": current["id"], "base_ref": current["default_branch"],
157-
"event_name": "push", "event_action": "push",
158-
"actor_login": actors[0], "triggering_actor": actors[1],
159-
"workflow_ref": os.environ["GITHUB_WORKFLOW_REF"], "workflow_sha": after,
160-
"run_id": positive(os.environ["GITHUB_RUN_ID"]), "run_attempt": positive(os.environ["GITHUB_RUN_ATTEMPT"]),
161-
},
162-
"event": {
163-
"repository": {"id": current["id"], "full_name": repository}, "ref": event["ref"],
164-
"before": before, "after": after, "created": False, "deleted": False, "forced": False,
165-
"commits": [{"id": sha} for sha in shas], "head_commit": {"id": after},
166-
},
167-
}
168-
169-
170-
def prepare_dispatch():
171-
snapshot = push_snapshot()
172-
content = json.dumps(snapshot, separators=(",", ":")).encode("utf-8")
173-
require(len(content) <= MAX_SOURCE_BYTES, "Source event exceeds 64 KiB")
174-
path = source_event_path()
175-
path.parent.mkdir(exist_ok=True)
176-
path.write_bytes(content)
177-
outputs({"source-event-path": path})
178-
print("Prepared identity-only team-memory source event")
179-
180-
181-
def dispatch():
182-
token = os.environ["DISPATCH_TOKEN"]
183-
require(token.strip(), "dispatch-token must be non-empty")
184-
snapshot = read_json(source_event_path(), MAX_SOURCE_BYTES)
185-
expected = push_snapshot()
186-
require(snapshot == expected and matching_metadata(snapshot["metadata"], expected["metadata"]),
187-
"Source event changed before dispatch")
188-
central = project(COORDINATOR, token)
189-
metadata = snapshot["metadata"]
190-
request = github_request(
191-
f"/repos/{COORDINATOR}/actions/workflows/team-memory-coordinator.yml/dispatches", token,
192-
{"ref": central["default_branch"], "inputs": {
193-
"source_repository": metadata["repository"], "source_run_id": str(metadata["run_id"]),
194-
"source_run_attempt": str(metadata["run_attempt"]),
195-
"source_artifact_id": str(positive(os.environ["SOURCE_ARTIFACT_ID"])),
196-
}},
197-
)
198-
with urllib.request.build_opener(NoRedirect()).open(request, timeout=30) as response:
199-
require(response.status in {200, 204}, "Dispatch was not acknowledged; do not retry blindly")
200-
print("Coordinator dispatch accepted; maintenance completion is reported by the coordinator run")
201-
202-
203126
def selection():
204127
repository = os.environ.get("SOURCE_REPOSITORY", "")
205128
require(repository in SOURCES, "Supply an allowlisted source_repository")
@@ -310,7 +233,6 @@ def preflight():
310233
and all(isinstance(item, dict) and set(item) == {"id"} for item in push["commits"])
311234
and push["head_commit"] == {"id": metadata["workflow_sha"]},
312235
"Invalid identity-only push artifact")
313-
validate_push(push, "refs/heads/" + current["default_branch"], metadata["workflow_sha"])
314236
try:
315237
envelope = client.prepare_push_memory(repository, current, push, {**metadata, **central})
316238
except client.SkippedRequest as skipped:
@@ -332,17 +254,14 @@ def preflight():
332254

333255
def run(command):
334256
try:
335-
{"prepare-dispatch": prepare_dispatch, "dispatch": dispatch, "select-source": select_source,
336-
"validate-dispatch": validate_dispatch, "preflight": preflight}[command]()
257+
{"select-source": select_source, "validate-dispatch": validate_dispatch, "preflight": preflight}[command]()
337258
except ValueError as error:
338259
raise SystemExit(f"::error::{error}") from None
339260
except Exception:
340-
message = ("Coordinator dispatch failed or its outcome is unknown; inspect coordinator runs before retrying"
341-
if command == "dispatch" else "Java team-memory validation failed; no agent request was sent")
342-
raise SystemExit(f"::error::{message}") from None
261+
raise SystemExit("::error::Java team-memory validation failed; no agent request was sent") from None
343262

344263

345264
if __name__ == "__main__":
346265
parser = argparse.ArgumentParser(description=__doc__)
347-
parser.add_argument("command", choices=("prepare-dispatch", "dispatch", "select-source", "validate-dispatch", "preflight"))
266+
parser.add_argument("command", choices=("select-source", "validate-dispatch", "preflight"))
348267
run(parser.parse_args().command)

0 commit comments

Comments
 (0)