Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8ca4975e5 | ||
|
|
f2e1bd6a0d | ||
|
|
b0cc9dad14 | ||
|
|
52304c7eb2 |
@@ -37,7 +37,6 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
commit: ${{ steps.ref.outputs.commit }}
|
commit: ${{ steps.ref.outputs.commit }}
|
||||||
tag: ${{ steps.ref.outputs.tag }}
|
tag: ${{ steps.ref.outputs.tag }}
|
||||||
runner: ${{ steps.ref.outputs.runner }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@@ -64,7 +63,7 @@ jobs:
|
|||||||
commit="$(git rev-parse --verify "refs/tags/$tag^{commit}")"
|
commit="$(git rev-parse --verify "refs/tags/$tag^{commit}")"
|
||||||
git merge-base --is-ancestor "$commit" origin/main || { echo 'Release tag must belong to main.' >&2; exit 1; }
|
git merge-base --is-ancestor "$commit" origin/main || { echo 'Release tag must belong to main.' >&2; exit 1; }
|
||||||
git checkout --detach "$commit"
|
git checkout --detach "$commit"
|
||||||
printf 'commit=%s\ntag=%s\nrunner=%s\n' "$commit" "$tag" "$MACOS_RUNNER" >> "$GITHUB_OUTPUT"
|
printf 'commit=%s\ntag=%s\n' "$commit" "$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
@@ -81,7 +80,8 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Sign, notarize, verify and publish macOS
|
name: Sign, notarize, verify and publish macOS
|
||||||
needs: prepare
|
needs: prepare
|
||||||
runs-on: ${{ needs.prepare.outputs.runner }}
|
# Gitea resolves runner labels before outputs from prepare are available.
|
||||||
|
runs-on: ${{ vars.MACOS_RUNNER }}
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
env:
|
env:
|
||||||
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
|
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
|
||||||
|
|||||||
@@ -94,7 +94,26 @@ export async function publishMacos({
|
|||||||
...args,
|
...args,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
const versioning = JSON.parse(s3('get-bucket-versioning', []));
|
const versioningOutput = s3('get-bucket-versioning', [
|
||||||
|
'--output',
|
||||||
|
'json',
|
||||||
|
]).trim();
|
||||||
|
let versioning;
|
||||||
|
try {
|
||||||
|
// AWS CLI prints nothing for a successful empty VersioningConfiguration.
|
||||||
|
versioning = JSON.parse(versioningOutput || '{}');
|
||||||
|
} catch {
|
||||||
|
throw new Error('Invalid JSON from S3 bucket versioning check.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!versioning ||
|
||||||
|
typeof versioning !== 'object' ||
|
||||||
|
Array.isArray(versioning)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid S3 bucket versioning response: expected an object.',
|
||||||
|
);
|
||||||
|
}
|
||||||
if (versioning.Status) {
|
if (versioning.Status) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Use an unversioned release bucket: Hetzner does not support conditional writes on versioned buckets.',
|
'Use an unversioned release bucket: Hetzner does not support conditional writes on versioned buckets.',
|
||||||
|
|||||||
@@ -86,13 +86,22 @@ test('release selection succeeds using Git alone without an administrative API t
|
|||||||
GITHUB_API_URL: 'http://127.0.0.1:1',
|
GITHUB_API_URL: 'http://127.0.0.1:1',
|
||||||
});
|
});
|
||||||
assert.equal(result.status, 0, result.stderr);
|
assert.equal(result.status, 0, result.stderr);
|
||||||
assert.equal(
|
assert.equal(result.output, `commit=${f.commit}\ntag=macos-v1.2.3\n`);
|
||||||
result.output,
|
|
||||||
`commit=${f.commit}\ntag=macos-v1.2.3\nrunner=macos-local\n`,
|
|
||||||
);
|
|
||||||
assert.equal(f.git(f.checkout, 'rev-parse', 'HEAD'), f.commit);
|
assert.equal(f.git(f.checkout, 'rev-parse', 'HEAD'), f.commit);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('macOS scheduling uses a repository variable while release steps remain gated by preparation', () => {
|
||||||
|
const release = workflow.split('\n release:\n')[1];
|
||||||
|
const runner = release.match(/^ {4}runs-on: (.+)$/m)?.[1];
|
||||||
|
assert.equal(runner, '${{ vars.MACOS_RUNNER }}');
|
||||||
|
assert.match(release, /^ {4}needs: prepare$/m);
|
||||||
|
assert.match(release, /ref: \$\{\{ needs\.prepare\.outputs\.commit \}\}/);
|
||||||
|
assert.match(
|
||||||
|
release,
|
||||||
|
/RELEASE_TAG: \$\{\{ needs\.prepare\.outputs\.tag \}\}/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('manual release on main accepts an existing tag without moving it', (t) => {
|
test('manual release on main accepts an existing tag without moving it', (t) => {
|
||||||
const f = fixture(t);
|
const f = fixture(t);
|
||||||
const result = f.run({
|
const result = f.run({
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ function fixture(t) {
|
|||||||
const key = value('--key');
|
const key = value('--key');
|
||||||
events.push({ operation, key });
|
events.push({ operation, key });
|
||||||
if (operation === 'get-bucket-versioning') {
|
if (operation === 'get-bucket-versioning') {
|
||||||
return '{}';
|
assert.equal(value('--output'), 'json');
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
if (operation === 'put-object') {
|
if (operation === 'put-object') {
|
||||||
assert.equal(value('--if-none-match'), '*');
|
assert.equal(value('--if-none-match'), '*');
|
||||||
@@ -103,6 +104,33 @@ test('uploads immutable files and writes completion record after public verifica
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const response of ['', ' \n\t', '{}']) {
|
||||||
|
test(`unversioned bucket response ${JSON.stringify(response)} permits a verified upload`, async (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
const run = f.run;
|
||||||
|
f.run = (command, args) => {
|
||||||
|
const result = run(command, args);
|
||||||
|
return args.includes('get-bucket-versioning') ? response : result;
|
||||||
|
};
|
||||||
|
await publishMacos(f);
|
||||||
|
assert.equal(f.objects.size, 3);
|
||||||
|
assert.equal(f.events.at(-1).operation, 'public-get');
|
||||||
|
assert(f.events.at(-1).key.endsWith('/release.json'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const response of ['{', 'null', '[]', '"Enabled"', 'false']) {
|
||||||
|
test(`invalid versioning response ${response} aborts before any upload`, async (t) => {
|
||||||
|
const f = fixture(t);
|
||||||
|
f.run = (_command, args) => {
|
||||||
|
assert(args.includes('get-bucket-versioning'));
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
await assert.rejects(publishMacos(f), /Invalid .*versioning/);
|
||||||
|
assert.equal(f.objects.size, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('retry accepts byte-identical objects without replacing them', async (t) => {
|
test('retry accepts byte-identical objects without replacing them', async (t) => {
|
||||||
const f = fixture(t);
|
const f = fixture(t);
|
||||||
await publishMacos(f);
|
await publishMacos(f);
|
||||||
|
|||||||
Reference in New Issue
Block a user