Files
Dress/.github/workflows/check_exif.yml
Asahina Mafuyu 16c356e6a9
All checks were successful
Mark stale issues and pull requests / stale (push) Successful in 4s
[Enhancement] 添加自动修复工作流 (#337)
* feat: add GitHub Action to automatically strip EXIF data and compress images via PR comments

* fix: 提升尝试压缩后修复失败的用户体验

* feat: 现在修复结果会直接基于源 comment 做 update

* fix: 修复工作流仍然报告失败的问题

* doc: 添加自动修复引导

* ci: 改用 `github.rest.repos.compareCommitsWithBasehead`

* ci: 增强检查工作流
2026-04-08 00:31:01 +08:00

143 lines
7.7 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Check Image EXIF Data
on:
pull_request_target:
types: [opened, synchronize, reopened]
paths: ['**/*.jpg', '**/*.jpeg', '**/*.png', '**/*.webp', '**/*.tiff', '**/*.tif', '**/*.heic', '**/*.heif', '**/*.avif', '**/*.gif']
permissions:
contents: read
pull-requests: write
issues: write
concurrency:
group: exif-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
check-exif:
runs-on: ubuntu-latest
steps:
- name: Install exiftool
run: |
command -v exiftool && exit 0
sudo apt-get install -y --no-install-recommends libimage-exiftool-perl \
|| (sudo apt-get update && sudo apt-get install -y --no-install-recommends libimage-exiftool-perl)
- name: Check EXIF in changed images
uses: actions/github-script@v8
with:
script: |
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const pr = context.payload.pull_request;
const { owner, repo } = context.repo;
const prNumber = pr.number;
const IMAGE_EXT = /\.(jpe?g|png|webp|tiff?|hei[cf]|avif|gif)$/i;
// 1. Collect changed image files
// Use compareCommitsWithBasehead (base.sha...head.sha) instead of pulls.listFiles.
// pulls.listFiles computes the diff against the merge base, which can be a very old ancestor
// when a contributor's fork is far behind main (e.g. after clicking "Update branch" which
// creates a merge commit). This causes hundreds of unrelated files to appear as "changed".
// compareCommitsWithBasehead diffs exactly what this PR's head adds on top of main's current
// HEAD, regardless of the fork's branch history.
const changedImages = [];
let comparePage = 1;
while (true) {
const { data: cmp } = await github.rest.repos.compareCommitsWithBasehead({
owner, repo,
basehead: `${pr.base.sha}...${pr.head.sha}`,
per_page: 100, page: comparePage,
});
for (const f of (cmp.files || [])) {
if (['added', 'modified', 'renamed', 'copied'].includes(f.status) && IMAGE_EXT.test(f.filename))
changedImages.push(f.filename);
}
if (!cmp.files || cmp.files.length < 100) break;
comparePage++;
}
if (!changedImages.length) { core.info('No changed images.'); return; }
core.info(`Checking ${changedImages.length} image(s) for EXIF data.`);
// 2. Download & check each image
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'exif-'));
const exifFiles = [];
for (const file of changedImages) {
const tmpFile = path.join(tmp, file.replace(/[\\/]/g, '__'));
try {
// '#' must be percent-encoded in the API path; otherwise GitHub treats it as a URL fragment.
const apiPath = file.replace(/#/g, '%23');
const { data } = await github.rest.repos.getContent({
owner: pr.head.repo.owner.login, repo: pr.head.repo.name,
path: apiPath, ref: pr.head.sha
});
let raw;
if (!Array.isArray(data) && data.content) raw = Buffer.from(data.content, 'base64');
else if (!Array.isArray(data) && data.download_url) {
const resp = await fetch(data.download_url);
if (!resp.ok) throw new Error(`Download ${resp.status}`);
raw = Buffer.from(await resp.arrayBuffer());
} else throw new Error('Cannot fetch file content');
fs.writeFileSync(tmpFile, raw);
// Check only high-sensitivity fields:
// GPS:* — precise location coordinates
// IPTC address fields — textual shooting address
// XMP location/contact fields — address & contact info written by editing software
const HIGH_SENS_ARGS = [
'-GPS:GPSLatitude', '-GPS:GPSLongitude', '-GPS:GPSAltitude',
'-GPS:GPSDateStamp', '-GPS:GPSTimeStamp',
'-GPS:GPSSpeed', '-GPS:GPSTrack', '-GPS:GPSImgDirection',
'-IPTC:City', '-IPTC:Country-PrimaryLocationName',
'-IPTC:Sub-location', '-IPTC:Province-State',
'-XMP-photoshop:City', '-XMP-photoshop:Country', '-XMP-photoshop:State',
'-XMP-iptcCore:CreatorWorkEmail', '-XMP-iptcCore:CreatorWorkTelephone',
'-XMP-iptcCore:CreatorCity', '-XMP-iptcCore:CreatorCountry',
'-XMP-iptcCore:CreatorPostalCode',
];
const safeFile = tmpFile.replace(/'/g, "'\\\\'");
// Filter out empty/undef/all-zero placeholder values (e.g. GPS IFD container with no real coords).
// `: *(undef|00:00:00)?$` matches lines like "GPSLatitude: ", "GPSAltitude: undef", "GPSTimeStamp: 00:00:00"
const count = parseInt(execSync(
`exiftool ${HIGH_SENS_ARGS.join(' ')} -S -q -- '${safeFile}' 2>/dev/null | grep -Ev ': *(undef|00:00:00)?$' | wc -l`,
{ encoding: 'utf-8', timeout: 10000 }
).trim()) || 0;
if (count > 0) { exifFiles.push(file); core.info(`Sensitive EXIF found: ${file} (${count} tags)`); }
} catch (e) {
// Download failures (e.g. diverged fork, encoding issues) are non-fatal.
// Skip the file with a warning rather than failing the entire check.
core.warning(`Skipping ${file}: ${e.message}`);
}
finally { try { fs.unlinkSync(tmpFile); } catch {} }
}
try { fs.rmdirSync(tmp); } catch {}
// 3. Report results
if (exifFiles.length) {
const fileList = exifFiles.map(f => `- ${f}`).join('\n');
const body = [
'我们检测到以下文件包含**高敏感 EXIF 信息**(如 GPS 坐标、地址或联系方式),请移除后再次提交:',
'We detected **high-sensitivity EXIF data** (e.g. GPS coordinates, address, or contact fields) in the following files. Please remove them and resubmit:',
'',
fileList,
'',
'**仅高敏感字段GPS / 地址 / 联系方式)需要移除,普通摄影参数(光圈、快门等)无需处理。**',
'**Only high-sensitivity fields (GPS / address / contact info) need to be removed. Regular photography parameters (aperture, shutter speed, etc.) are fine to keep.**',
'',
`📖 [EXIF 说明 / EXIF Guide](https://github.com/${owner}/${repo}/blob/master/EXIF.md) · [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/master/CONTRIBUTING.md)`,
'---',
'',
'<small>温馨提示: 您可以直接使用 `/auto-fix` 命令来自动修复这些文件。</small>',
'<small>Warm reminder: You can use the `/auto-fix` command to automatically fix these files.</small>'
].join('\n');
try { await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); }
catch (e) { core.warning(`Cannot comment: ${e.message}. Check repo Settings > Actions > Workflow permissions.`); }
core.setFailed('EXIF data detected. Please remove EXIF data for privacy protection.');
} else {
core.info('No EXIF data found. ✅');
}