Skip to content
Source
Sign inSign up

mobile-fingerprint-label.yml

189 lines7.2 KB

1name: Mobile Fingerprint Label
2​
3# Trusted half of mobile-fingerprint-check.yml. That workflow runs PR code
4# (fork PRs included) with a read-only token and records which platforms'
5# fingerprints changed. This one runs from main with a write token and syncs
6# the "πŸ“± Native Change" label. It never checks out or runs PR code.
7#
8# A PR can edit the check workflow it runs under, so the recorded result is
9# untrusted data: it is validated, and at worst a PR mislabels itself.
10​
11on:
12 workflow_run:
13 workflows: [Mobile Fingerprint Check]
14 types: [completed]
15​
16permissions:
17 contents: read
18​
19jobs:
20 label:
21 name: Sync native change label
22 # A cancelled run was superseded by a newer push, whose run labels instead.
23 if: >-
24 github.event.workflow_run.event == 'pull_request' &&
25 github.event.workflow_run.conclusion == 'success'
26 runs-on: ubuntu-24.04
27 timeout-minutes: 5
28 permissions:
29 actions: read
30 contents: read
31 issues: write
32 pull-requests: write
33 steps:
34 - id: pr
35 name: Resolve the pull request behind the check
36 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
37 with:
38 script: |
39 const run = context.payload.workflow_run;
40 const { owner, repo } = context.repo;
41​
42 const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
43 owner,
44 repo,
45 run_id: run.id,
46 per_page: 100,
47 });
48 const results = artifacts.filter(
49 (artifact) => artifact.name === "fingerprint-result" && !artifact.expired,
50 );
51 if (results.length !== 1) {
52 core.info(`Expected one fingerprint-result artifact; found ${results.length}. Skipping.`);
53 return;
54 }
55​
56 // workflow_run.pull_requests and the commit-to-PR lookup are both
57 // empty for fork PRs, so list open PRs by head owner and branch.
58 // Requiring the PR's current head to be the checked commit keeps a
59 // slow run for an older push from overwriting a newer result.
60 const open = await github.paginate(github.rest.pulls.list, {
61 owner,
62 repo,
63 state: "open",
64 head: `${run.head_repository?.owner?.login}:${run.head_branch}`,
65 per_page: 100,
66 });
67 const matching = open.filter(
68 (candidate) =>
69 candidate.state === "open" &&
70 candidate.head.sha === run.head_sha &&
71 candidate.head.ref === run.head_branch &&
72 candidate.head.repo?.full_name === run.head_repository?.full_name,
73 );
74 if (matching.length !== 1) {
75 core.info(`Expected one open PR at ${run.head_sha}; found ${matching.length}. Skipping.`);
76 return;
77 }
78​
79 core.setOutput("artifact_id", String(results[0].id));
80 core.setOutput("pr_number", String(matching[0].number));
81​
82 # Read the archive as bytes; never extract PR-produced files into a
83 # workspace this job runs code from.
84 - id: result
85 name: Read changed platforms
86 if: steps.pr.outputs.pr_number != ''
87 shell: bash
88 env:
89 GH_TOKEN: ${{ github.token }}
90 ARTIFACT_ID: ${{ steps.pr.outputs.artifact_id }}
91 run: |
92 set -euo pipefail
93 gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}/zip" > "$RUNNER_TEMP/fingerprint-result.zip"
94 changed="$(unzip -p "$RUNNER_TEMP/fingerprint-result.zip" changed-platforms)"
95 if [[ ! "$changed" =~ ^((ios|android)( (ios|android))?)?$ ]]; then
96 echo "Unexpected changed-platforms value '$changed'." >&2
97 exit 1
98 fi
99 echo "changed_platforms=$changed" >> "$GITHUB_OUTPUT"
100​
101 - name: Sync native change label
102 if: steps.pr.outputs.pr_number != ''
103 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
104 env:
105 PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
106 CHANGED_PLATFORMS: ${{ steps.result.outputs.changed_platforms }}
107 with:
108 script: |
109 const managedLabel = {
110 name: "πŸ“± Native Change",
111 color: "d93f0b",
112 description:
113 "Changes the native fingerprint; merging blocks production OTAs until a new store build ships.",
114 };
115 const nativeChanged = (process.env.CHANGED_PLATFORMS ?? "").trim() !== "";
116 const issueNumber = Number(process.env.PR_NUMBER);
117​
118 try {
119 const { data: existing } = await github.rest.issues.getLabel({
120 owner: context.repo.owner,
121 repo: context.repo.repo,
122 name: managedLabel.name,
123 });
124​
125 if (
126 existing.color !== managedLabel.color ||
127 (existing.description ?? "") !== managedLabel.description
128 ) {
129 await github.rest.issues.updateLabel({
130 owner: context.repo.owner,
131 repo: context.repo.repo,
132 name: managedLabel.name,
133 color: managedLabel.color,
134 description: managedLabel.description,
135 });
136 }
137 } catch (error) {
138 if (error.status !== 404) {
139 throw error;
140 }
141​
142 try {
143 await github.rest.issues.createLabel({
144 owner: context.repo.owner,
145 repo: context.repo.repo,
146 name: managedLabel.name,
147 color: managedLabel.color,
148 description: managedLabel.description,
149 });
150 } catch (createError) {
151 if (createError.status !== 422) {
152 throw createError;
153 }
154 }
155 }
156​
157 const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
158 owner: context.repo.owner,
159 repo: context.repo.repo,
160 issue_number: issueNumber,
161 per_page: 100,
162 });
163 const hasLabel = currentLabels.some((label) => label.name === managedLabel.name);
164​
165 if (nativeChanged && !hasLabel) {
166 await github.rest.issues.addLabels({
167 owner: context.repo.owner,
168 repo: context.repo.repo,
169 issue_number: issueNumber,
170 labels: [managedLabel.name],
171 });
172 } else if (!nativeChanged && hasLabel) {
173 try {
174 await github.rest.issues.removeLabel({
175 owner: context.repo.owner,
176 repo: context.repo.repo,
177 issue_number: issueNumber,
178 name: managedLabel.name,
179 });
180 } catch (removeError) {
181 if (removeError.status !== 404) {
182 throw removeError;
183 }
184 }
185 }
186​
187 core.info(
188 `PR #${issueNumber}: native fingerprint ${nativeChanged ? `changed (${process.env.CHANGED_PLATFORMS})` : "unchanged"}`,
189 );