Files
Dress/.github/workflows/check_exif.yml
Lian 2feab89b4e 增强工作流, 更新文档 (#338)
* fix(workflows:check_exif): 修改exiftool命令仅检测敏感信息

* docs(EXIF): 添加 EXIF 的说明文档

* docs(GUIDE): 完善新手引导文档

* fix(workflows:check_exif): 修改diff计算方式

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Yueosa <172176062+Yueosa@users.noreply.github.com>
2026-04-01 11:07:07 +08:00

139 lines
7.4 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)`
].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. ✅');
}