src/oscillators: Any PCM with at least 512 samples can be a wavetable. #89
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Pin AMY in tulipcc (on merge) | |
| # When an AMY PR is MERGED to main, open a PR on shorepine/tulipcc that bumps the | |
| # `amy` submodule pointer to the just-merged commit. That tulipcc PR runs the full | |
| # tulipcc CI (AMYboard preview, Tulip preview, desktop build) against this AMY, and | |
| # is left for a human to test and MERGE MANUALLY — it's the post-merge, whole-system | |
| # integration check that complements the per-PR AMYboard HW bench | |
| # (amyboard-hwci-trigger.yml), which still runs BEFORE merge on the AMY PR itself. | |
| # | |
| # WHY pull_request_target (not pull_request): we need repo secrets (HWCI_BRIDGE_TOKEN) | |
| # to push a branch + open a PR on tulipcc. A fork PR's `pull_request` run is denied | |
| # all secrets (hard GitHub rule), so a merged fork PR (rt-rtos, drepetto, ...) would | |
| # silently produce no tulipcc PR — exactly the trap the HW-CI trigger hit. With | |
| # pull_request_target the job runs in THIS repo's trusted base context (has secrets, | |
| # always main's workflow). It is SAFE because this job NEVER checks out or runs the | |
| # PR's code: it only reads PR metadata via the API and bumps a submodule gitlink in | |
| # tulipcc via the Git Data API. Do not add a checkout of the PR head here. | |
| # | |
| # WHY it doesn't loop / fire on release bumps: the release.yml version-bump PRs are | |
| # merged with GITHUB_TOKEN, and GITHUB_TOKEN merges never trigger workflow runs — so | |
| # this won't fire for them. The `!startsWith(... 'bump-')` guard is belt-and-suspenders | |
| # in case a bump PR is ever merged by hand. | |
| # | |
| # WHY a PAT and not GITHUB_TOKEN opens the tulipcc PR: a PR opened with GITHUB_TOKEN | |
| # triggers no `pull_request` workflows, so tulipcc CI would NOT run on it — defeating | |
| # the point. A PAT (HWCI_BRIDGE_TOKEN) is a user identity, so the PR it opens triggers | |
| # tulipcc's CI normally. | |
| # | |
| # TOKEN: reuses HWCI_BRIDGE_TOKEN (the same fine-grained PAT the HW-CI bridge uses). | |
| # It already has `contents: write` on shorepine/tulipcc and `issues/pull-requests: | |
| # write` on shorepine/amy. For THIS workflow it ALSO needs **`pull-requests: write` | |
| # on shorepine/tulipcc** (to open the bump PR). If that scope is missing the "Open | |
| # the bump PR on tulipcc" step fails with 403 — add it to the PAT and re-save the | |
| # secret. See the amy-hwci-bridge note for the token's owner/expiry. | |
| on: | |
| pull_request_target: | |
| types: [closed] | |
| # Only code tulipcc actually consumes: the C sources it compiles into firmware / | |
| # desktop (src/**) and the frozen Python module (amy/**). A docs/tests/CI-only | |
| # merge changes nothing tulipcc builds, so it shouldn't open a tulipcc PR. Widen | |
| # this list if tulipcc ever starts pulling other paths out of the submodule. | |
| paths: | |
| - 'src/**' | |
| - 'amy/**' | |
| - '.github/workflows/tulipcc-pin.yml' | |
| concurrency: | |
| group: tulipcc-pin-${{ github.event.pull_request.number }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read # GITHUB_TOKEN: read this merged PR's commits/files | |
| pull-requests: read | |
| jobs: | |
| pin: | |
| # Only on a real MERGE (not a plain close), and never for a release version bump. | |
| if: >- | |
| github.event.pull_request.merged == true && | |
| !startsWith(github.event.pull_request.head.ref, 'bump-') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Gather merged-PR details and build the tulipcc PR body | |
| id: gather | |
| uses: actions/github-script@v7 | |
| with: | |
| # Built-in token: read-only on this repo, enough to read the merged PR. | |
| github-token: ${{ github.token }} | |
| script: | | |
| const fs = require('fs'); | |
| const pr = context.payload.pull_request; | |
| const owner = context.repo.owner; // shorepine | |
| const repo = context.repo.repo; // amy | |
| // Pin the exact commit the merge produced on main. Using merge_commit_sha | |
| // (not main's tip) means we pin the code under test and never race with | |
| // release.yml, which advances main with a library.properties bump commit | |
| // right after this same merge. | |
| let sha = pr.merge_commit_sha; | |
| if (!sha) { | |
| const { data } = await github.rest.pulls.get({ owner, repo, pull_number: pr.number }); | |
| sha = data.merge_commit_sha; | |
| } | |
| const short = sha.slice(0, 9); | |
| const author = (pr.user && pr.user.login) || 'unknown'; | |
| // The person who clicked Merge — who we want to own the tulipcc merge too. | |
| const merger = (pr.merged_by && pr.merged_by.login) || author; | |
| // Concrete "what changed": the PR's own commits + a file-change summary. | |
| const commits = await github.paginate(github.rest.pulls.listCommits, { | |
| owner, repo, pull_number: pr.number, per_page: 100, | |
| }); | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner, repo, pull_number: pr.number, per_page: 100, | |
| }); | |
| const adds = files.reduce((n, f) => n + f.additions, 0); | |
| const dels = files.reduce((n, f) => n + f.deletions, 0); | |
| const COMMIT_CAP = 40; | |
| const commitLines = commits.slice(0, COMMIT_CAP).map(c => { | |
| const subj = (c.commit.message.split('\n')[0] || '').trim(); | |
| return `- [\`${c.sha.slice(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${c.sha}) ${subj}`; | |
| }); | |
| if (commits.length > COMMIT_CAP) { | |
| commitLines.push(`- …and ${commits.length - COMMIT_CAP} more commit(s)`); | |
| } | |
| const FILE_CAP = 50; | |
| const fileLines = files.slice(0, FILE_CAP).map(f => `- \`${f.filename}\` (+${f.additions}/-${f.deletions})`); | |
| if (files.length > FILE_CAP) { | |
| fileLines.push(`- …and ${files.length - FILE_CAP} more file(s)`); | |
| } | |
| // Markdown only — written to a file, never interpolated into a shell or a | |
| // second script, so the (already-merged, vetted) PR title/commit text | |
| // can't inject anything. The `${owner}/${repo}#${pr.number}` reference | |
| // back-links this on the AMY PR (notifying its participants), and the | |
| // `cc @${merger}` line mails the person who merged it. | |
| const body = [ | |
| '<!-- amy-tulipcc-pin -->', | |
| `## ⛓️ Pin \`amy\` → \`${short}\` (validates ${owner}/${repo}#${pr.number})`, | |
| '', | |
| `\`amy\` was just merged to **${owner}/${repo}@main**. This PR bumps the \`amy\` submodule`, | |
| 'pointer to that commit so the full tulipcc CI (AMYboard preview, Tulip preview, desktop', | |
| 'build) runs against it.', | |
| '', | |
| '**Please test, then merge this PR manually** to move tulipcc onto the latest AMY.', | |
| '', | |
| '| | |', | |
| '|---|---|', | |
| `| Merged AMY PR | ${owner}/${repo}#${pr.number} — ${pr.title} |`, | |
| `| Author | @${author} |`, | |
| `| Merged by | @${merger} |`, | |
| `| AMY commit | ${owner}/${repo}@${sha} |`, | |
| '', | |
| '### What changed in this AMY PR', | |
| '', | |
| ...commitLines, | |
| '', | |
| `<details><summary>${files.length} file(s) changed (+${adds}/-${dels})</summary>`, | |
| '', | |
| ...fileLines, | |
| '</details>', | |
| '', | |
| '---', | |
| `cc @${merger} — you merged the AMY change above; please review the tulipcc CI on this PR and merge it here when green.`, | |
| ].join('\n'); | |
| fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/tulipcc_pr_body.md`, body); | |
| core.setOutput('amy_sha', sha); | |
| core.setOutput('short', short); | |
| core.setOutput('merger', merger); | |
| core.setOutput('amy_pr', String(pr.number)); | |
| - name: Refresh tulipcc's refdocs/amy snapshot for the new pin | |
| env: | |
| AMY_SHA: ${{ steps.gather.outputs.amy_sha }} | |
| run: | | |
| # tulipcc CI's refdocs-fresh check fails any PR that moves the amy gitlink | |
| # without regenerating tulip/server/refdocs/amy (its _VENDORED_FROM.txt | |
| # stamps the source SHA). Run tulipcc's own sync script against the new pin | |
| # and hand the changed files to the next step to fold into the bump commit. | |
| # | |
| # SAFE under pull_request_target: this clones shorepine/tulipcc@main | |
| # (trusted) and the script reads amy docs via GitHub raw at the merged | |
| # main SHA — it never checks out or executes this PR's code. | |
| # | |
| # Best-effort: on any failure (e.g. a tulipcc@main sync_amy_docs.py | |
| # without the --sha flag), leave the changes list empty and open the | |
| # pin PR without the snapshot, exactly as before this step existed. | |
| : > "$GITHUB_WORKSPACE/refdocs_changes.txt" | |
| if git clone --depth 1 https://github.com/shorepine/tulipcc "$RUNNER_TEMP/tulipcc" \ | |
| && python3 "$RUNNER_TEMP/tulipcc/tulip/server/sync_amy_docs.py" --sha "$AMY_SHA"; then | |
| git -C "$RUNNER_TEMP/tulipcc" add -A tulip/server/refdocs/amy | |
| git -C "$RUNNER_TEMP/tulipcc" -c diff.renames=false diff --cached --name-status \ | |
| -- tulip/server/refdocs/amy > "$GITHUB_WORKSPACE/refdocs_changes.txt" | |
| else | |
| echo "::warning::refdocs refresh failed; opening the pin PR without a snapshot refresh" | |
| fi | |
| echo "refdocs changes for the bump commit:" | |
| cat "$GITHUB_WORKSPACE/refdocs_changes.txt" | |
| - name: Open the bump PR on tulipcc | |
| id: pin | |
| uses: actions/github-script@v7 | |
| env: | |
| AMY_SHA: ${{ steps.gather.outputs.amy_sha }} | |
| SHORT: ${{ steps.gather.outputs.short }} | |
| MERGER: ${{ steps.gather.outputs.merger }} | |
| AMY_PR: ${{ steps.gather.outputs.amy_pr }} | |
| with: | |
| # Fine-grained PAT with contents:write + pull-requests:write on tulipcc. | |
| github-token: ${{ secrets.HWCI_BRIDGE_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const owner = 'shorepine', repo = 'tulipcc', base = 'main'; | |
| const amySha = process.env.AMY_SHA; | |
| const short = process.env.SHORT; | |
| const merger = process.env.MERGER; | |
| const amyPr = process.env.AMY_PR; | |
| const branch = `amy-pin/${short}`; | |
| const body = fs.readFileSync(`${process.env.GITHUB_WORKSPACE}/tulipcc_pr_body.md`, 'utf8'); | |
| const title = `Pin amy to ${short} (amy#${amyPr})`; | |
| // Idempotent: if this exact pin is already open (manual re-run), reuse it. | |
| const open = await github.rest.pulls.list({ owner, repo, state: 'open', head: `${owner}:${branch}` }); | |
| if (open.data.length) { | |
| core.notice(`tulipcc PR already open for ${branch}: ${open.data[0].html_url}`); | |
| core.setOutput('pr_url', open.data[0].html_url); | |
| return; | |
| } | |
| // Build the bump commit straight on tulipcc with the Git Data API — no | |
| // checkout, no submodule fetch. A 160000-mode tree entry IS a submodule | |
| // gitlink; the amy commit need not exist in tulipcc for this to commit. | |
| const baseRef = await github.rest.git.getRef({ owner, repo, ref: `heads/${base}` }); | |
| const baseSha = baseRef.data.object.sha; | |
| const baseCommit = await github.rest.git.getCommit({ owner, repo, commit_sha: baseSha }); | |
| // The gitlink bump, plus the refreshed refdocs/amy snapshot (if the | |
| // previous step produced one) so the refdocs-fresh check passes. | |
| const entries = [{ path: 'amy', mode: '160000', type: 'commit', sha: amySha }]; | |
| let refdocsChanges = []; | |
| try { | |
| refdocsChanges = fs.readFileSync(`${process.env.GITHUB_WORKSPACE}/refdocs_changes.txt`, 'utf8') | |
| .split('\n').map(l => l.trim()).filter(Boolean); | |
| } catch (e) { /* no refresh available; bump the gitlink alone */ } | |
| for (const line of refdocsChanges) { | |
| const [status, file] = line.split('\t'); | |
| if (!file || !file.startsWith('tulip/server/refdocs/amy/')) continue; | |
| if (status === 'D') { | |
| // sha: null deletes the path from the tree. | |
| entries.push({ path: file, mode: '100644', type: 'blob', sha: null }); | |
| } else { | |
| const content = fs.readFileSync(`${process.env.RUNNER_TEMP}/tulipcc/${file}`, 'utf8'); | |
| entries.push({ path: file, mode: '100644', type: 'blob', content }); | |
| } | |
| } | |
| const tree = await github.rest.git.createTree({ | |
| owner, repo, base_tree: baseCommit.data.tree.sha, | |
| tree: entries, | |
| }); | |
| // Nothing to do if tulipcc already points at this amy commit. | |
| if (tree.data.sha === baseCommit.data.tree.sha) { | |
| core.notice(`tulipcc main already pins amy@${short}; nothing to open.`); | |
| return; | |
| } | |
| const refdocsNote = entries.length > 1 | |
| ? '\n\nIncludes the regenerated tulip/server/refdocs/amy snapshot for this pin.' | |
| : ''; | |
| const commit = await github.rest.git.createCommit({ | |
| owner, repo, | |
| message: `Bump amy submodule to ${short} (amy#${amyPr})` + refdocsNote, | |
| tree: tree.data.sha, | |
| parents: [baseSha], | |
| }); | |
| try { | |
| await github.rest.git.createRef({ owner, repo, ref: `refs/heads/${branch}`, sha: commit.data.sha }); | |
| } catch (e) { | |
| if (e.status === 422) { | |
| // Stale branch from a prior run — repoint it. | |
| await github.rest.git.updateRef({ owner, repo, ref: `heads/${branch}`, sha: commit.data.sha, force: true }); | |
| } else { | |
| throw e; | |
| } | |
| } | |
| const pr = await github.rest.pulls.create({ owner, repo, base, head: branch, title, body }); | |
| core.setOutput('pr_url', pr.data.html_url); | |
| core.info(`Opened tulipcc PR: ${pr.data.html_url}`); | |
| // Best-effort: assign the AMY merger so the manual merge here is clearly theirs. | |
| try { | |
| await github.rest.issues.addAssignees({ owner, repo, issue_number: pr.data.number, assignees: [merger] }); | |
| } catch (e) { | |
| core.warning(`Could not assign @${merger} on the tulipcc PR: ${e.message}`); | |
| } | |
| - name: Link the tulipcc PR back on the AMY PR | |
| if: steps.pin.outputs.pr_url != '' | |
| uses: actions/github-script@v7 | |
| env: | |
| PR_URL: ${{ steps.pin.outputs.pr_url }} | |
| with: | |
| # Bridge token so the note also posts on merged fork PRs (whose GITHUB_TOKEN | |
| # is read-only). It has issues:write on shorepine/amy. | |
| github-token: ${{ secrets.HWCI_BRIDGE_TOKEN }} | |
| script: | | |
| const url = process.env.PR_URL; | |
| const marker = '<!-- amy-tulipcc-pin-back -->'; | |
| const body = [ | |
| marker, | |
| '### ⛓️ tulipcc integration PR opened', | |
| '', | |
| `This merge was pinned into tulipcc for full-system CI: ${url}`, | |
| '', | |
| 'Test it there and merge that PR to move tulipcc onto this AMY.', | |
| ].join('\n'); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); | |
| const existing = comments.find(c => c.body && c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body }); | |
| } |