Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 1 addition & 20 deletions .github/workflows/openpi-feishu-pr-notification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,8 @@ jobs:
timeout-minutes: 5
if: ${{ github.event_name == 'pull_request_target' && !github.event.pull_request.draft }}
steps:
- name: Check notification configuration
id: config
env:
FEISHU_PR_BOT_WEBHOOK: ${{ secrets.FEISHU_PR_BOT_WEBHOOK }}
FEISHU_PR_BOT_SECRET: ${{ secrets.FEISHU_PR_BOT_SECRET }}
run: |
if test -z "${FEISHU_PR_BOT_WEBHOOK}" && test -z "${FEISHU_PR_BOT_SECRET}"; then
echo "Feishu bot secrets are not configured; skipping notification."
echo "enabled=false" >>"${GITHUB_OUTPUT}"
exit 0
fi

if test -z "${FEISHU_PR_BOT_WEBHOOK}" || test -z "${FEISHU_PR_BOT_SECRET}"; then
echo "Both FEISHU_PR_BOT_WEBHOOK and FEISHU_PR_BOT_SECRET must be configured."
exit 1
fi

echo "enabled=true" >>"${GITHUB_OUTPUT}"
- name: Send PR notification
if: ${{ steps.config.outputs.enabled == 'true' }}
uses: openpi-dev/automation/actions/feishu-pr-notification@2ff72314d7aa18d373490ccd615b8d69e91cf408
uses: openpi-dev/automation/actions/feishu-pr-notification@c631a31ed6f1851aebe0c0c20cf898f8013a5fbb
env:
FEISHU_PR_BOT_WEBHOOK: ${{ secrets.FEISHU_PR_BOT_WEBHOOK }}
FEISHU_PR_BOT_SECRET: ${{ secrets.FEISHU_PR_BOT_SECRET }}
Expand Down
40 changes: 12 additions & 28 deletions .github/workflows/openpi-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,12 @@ jobs:
steps:
- name: Resolve release source
id: source
env:
RELEASE_TAG: ${{ inputs.tag }}
EVENT_NAME: ${{ github.event_name }}
REF: ${{ github.ref }}
run: |
case "${EVENT_NAME}" in
workflow_dispatch)
test "${REF}" = "refs/heads/main"
;;
push)
test "${REF}" = "refs/tags/${RELEASE_TAG}"
;;
*)
echo "Unsupported release event: ${EVENT_NAME}"
exit 1
;;
esac
git check-ref-format "refs/tags/${RELEASE_TAG}"
case "${RELEASE_TAG}" in
v*.*.*) ;;
*) exit 1 ;;
esac
printf 'tag=%s\n' "${RELEASE_TAG}" >>"${GITHUB_OUTPUT}"
uses: openpi-dev/automation/actions/openpi-release-validation@c631a31ed6f1851aebe0c0c20cf898f8013a5fbb
with:
mode: resolve
release_tag: ${{ inputs.tag }}
event_name: ${{ github.event_name }}
ref: ${{ github.ref }}
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
Expand All @@ -58,12 +41,13 @@ jobs:
run: bun install --frozen-lockfile
- run: bun run check
- run: bun run test
- name: Verify tagged release source
env:
RELEASE_TAG: ${{ steps.source.outputs.tag }}
- name: Verify package version
uses: openpi-dev/automation/actions/openpi-release-validation@c631a31ed6f1851aebe0c0c20cf898f8013a5fbb
with:
mode: verify-package
release_tag: ${{ steps.source.outputs.tag }}
- name: Verify tagged release ancestry
run: |
version="$(node -p "require('./package.json').version")"
test "${RELEASE_TAG}" = "v${version}"
git fetch --no-tags origin main:refs/remotes/origin/main
git merge-base --is-ancestor HEAD origin/main
- name: Verify package contents
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ remain in each caller repository.
```sh
npm test
```

The suite exercises Feishu metadata sanitization, injection resistance, Unicode
bounds, secret-pair handling, HMAC payloads, and response failures. It also
executes release event/tag/package-version validation and locks the reusable
workflow boundaries for draft suppression, OIDC, ancestry, and single-artifact
publication.
27 changes: 25 additions & 2 deletions actions/feishu-pr-notification/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ function isSuccessfulResponse(payload) {
);
}

function resolveNotificationConfiguration(webhook, secret) {
const hasWebhook = typeof webhook === "string" && webhook.length > 0;
const hasSecret = typeof secret === "string" && secret.length > 0;

if (!hasWebhook && !hasSecret) return { enabled: false };
if (!hasWebhook || !hasSecret) {
throw new Error(
"Both FEISHU_PR_BOT_WEBHOOK and FEISHU_PR_BOT_SECRET must be configured.",
);
}
return { enabled: true, webhook, secret };
}

async function sendNotification({
event,
repository,
Expand Down Expand Up @@ -119,12 +132,21 @@ async function sendNotification({
}

async function main() {
const configuration = resolveNotificationConfiguration(
process.env.FEISHU_PR_BOT_WEBHOOK,
process.env.FEISHU_PR_BOT_SECRET,
);
if (!configuration.enabled) {
console.log("Feishu bot secrets are not configured; skipping notification.");
return;
}

const event = JSON.parse(fs.readFileSync(process.env.EVENT_PATH, "utf8"));
await sendNotification({
event,
repository: process.env.REPOSITORY,
webhook: process.env.FEISHU_PR_BOT_WEBHOOK,
secret: process.env.FEISHU_PR_BOT_SECRET,
webhook: configuration.webhook,
secret: configuration.secret,
});
console.log("Feishu PR notification sent.");
}
Expand All @@ -139,6 +161,7 @@ if (require.main === module) {
module.exports = {
formatNotificationText,
isSuccessfulResponse,
resolveNotificationConfiguration,
sanitizeFeishuField,
sendNotification,
};
21 changes: 21 additions & 0 deletions actions/openpi-release-validation/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: OpenPI release validation
description: Validate an OpenPI release event, tag, and checked-out package version
inputs:
mode:
description: Validation phase (`resolve` or `verify-package`)
required: true
release_tag:
description: Existing version tag selected by the caller
required: true
event_name:
description: Caller event name for source resolution
required: false
ref:
description: Caller Git ref for source resolution
required: false
outputs:
tag:
description: Validated release tag
runs:
using: node24
main: index.cjs
113 changes: 113 additions & 0 deletions actions/openpi-release-validation/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const fs = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");

function requireNonempty(value, label) {
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`${label} is required.`);
}
return value.trim();
}

function checkReleaseRef(tag) {
const result = spawnSync("git", ["check-ref-format", `refs/tags/${tag}`], {
encoding: "utf8",
});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(`Invalid release tag: ${tag}`);
}

function validateReleaseTag(tag, checkRef = checkReleaseRef) {
const normalized = requireNonempty(tag, "release_tag");
checkRef(normalized);
if (!/^v[^/]*\.[^/]*\.[^/]*$/u.test(normalized)) {
throw new Error(`Release tag must match v*.*.*: ${normalized}`);
}
return normalized;
}

function resolveReleaseSource({ eventName, ref, releaseTag, checkRef }) {
const tag = validateReleaseTag(releaseTag, checkRef);
const event = requireNonempty(eventName, "event_name");
const sourceRef = requireNonempty(ref, "ref");

if (event === "workflow_dispatch") {
if (sourceRef !== "refs/heads/main") {
throw new Error("Manual releases must be dispatched from main.");
}
} else if (event === "push") {
if (sourceRef !== `refs/tags/${tag}`) {
throw new Error("A release push must match the selected version tag.");
}
} else {
throw new Error(`Unsupported release event: ${event}`);
}

return tag;
}

function readPackageVersion(workspace) {
const packagePath = path.join(workspace, "package.json");
const parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
return requireNonempty(parsed.version, "package.json version");
}

function verifyPackageVersion({ releaseTag, workspace, checkRef }) {
const tag = validateReleaseTag(releaseTag, checkRef);
const version = readPackageVersion(requireNonempty(workspace, "GITHUB_WORKSPACE"));
if (tag !== `v${version}`) {
throw new Error(`Release tag ${tag} does not match package version ${version}.`);
}
return tag;
}

function appendOutput(name, value, outputPath) {
fs.appendFileSync(outputPath, `${name}=${value}\n`, "utf8");
}

function input(name) {
return process.env[`INPUT_${name.toUpperCase()}`] ?? "";
}

function main() {
const mode = requireNonempty(input("mode"), "mode");
const releaseTag = input("release_tag");
let tag;

if (mode === "resolve") {
tag = resolveReleaseSource({
eventName: input("event_name"),
ref: input("ref"),
releaseTag,
});
} else if (mode === "verify-package") {
tag = verifyPackageVersion({
releaseTag,
workspace: process.env.GITHUB_WORKSPACE,
});
} else {
throw new Error(`Unsupported validation mode: ${mode}`);
}

appendOutput(
"tag",
tag,
requireNonempty(process.env.GITHUB_OUTPUT, "GITHUB_OUTPUT"),
);
}

if (require.main === module) {
try {
main();
} catch (error) {
console.error(error);
process.exit(1);
}
}

module.exports = {
readPackageVersion,
resolveReleaseSource,
validateReleaseTag,
verifyPackageVersion,
};
Loading
Loading