{"id":50916703,"url":"https://github.com/sidick/aminet-release-action","last_synced_at":"2026-06-16T16:01:49.148Z","repository":{"id":364569009,"uuid":"1268382003","full_name":"sidick/aminet-release-action","owner":"sidick","description":"Aminet release GitHub action","archived":false,"fork":false,"pushed_at":"2026-06-13T15:28:34.000Z","size":114,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-13T16:04:46.554Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sidick.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-13T13:15:53.000Z","updated_at":"2026-06-13T15:27:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sidick/aminet-release-action","commit_stats":null,"previous_names":["sidick/aminet-release-action"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/sidick/aminet-release-action","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sidick%2Faminet-release-action","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sidick%2Faminet-release-action/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sidick%2Faminet-release-action/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sidick%2Faminet-release-action/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sidick","download_url":"https://codeload.github.com/sidick/aminet-release-action/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sidick%2Faminet-release-action/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34412795,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-16T16:01:47.225Z","updated_at":"2026-06-16T16:01:49.135Z","avatar_url":"https://github.com/sidick.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# aminet-release-action\n\nA GitHub Action that validates a pre-built Aminet upload (`.lha`/`.zip`/etc. plus a `.readme`), uploads it to Aminet via anonymous FTP, and optionally attaches both files to a matching GitHub Release.\n\nPackaging the archive is out of scope — bring your own build step.\n\n## Features\n\n- **Readme validation** against the [Aminet wiki spec](https://wiki.aminet.net/The_Readme_file): required fields, `Short:` length, multi-architecture syntax (e.g. `m68k-amigaos; ppc-morphos \u003e= 1.4.0`), distribution values, filename rules, body line length.\n- **Anonymous FTP upload** to `main.aminet.net:/new` per the [Aminet upload procedure](https://wiki.aminet.net/Uploading_instructions). The FTP password (an email) is taken from the `uploader-email` input or, if that's empty, extracted from the readme's `Uploader:` field. CR+LF readmes are silently normalised to LF on the wire.\n- **Inline PR annotations** — validation failures surface as `::error::` / `::warning::` annotations in the GitHub UI with file/line locations.\n- **Optional `inject-version`** rewrites the readme's `Version:` field from the git tag before validation.\n- **Optional `check-requires`** HTTP-HEADs file-path entries in the readme's `Requires:` field against aminet.net to catch typos and dangling references.\n- **Release asset attachment** — on tag pushes, attaches the upload file and `.readme` to the matching GitHub Release.\n\n## Usage\n\n### Release workflow (tag-driven upload)\n\n```yaml\nname: Release\non:\n  push:\n    tags: ['v*']\n\npermissions:\n  contents: write   # needed to attach the upload to the GitHub Release\n\njobs:\n  aminet:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Build the archive\n        run: make dist   # whatever produces dist/MyTool.lha + dist/MyTool.readme\n\n      - uses: sidick/aminet-release-action@v1\n        with:\n          filename: dist/MyTool.lha\n          readme: dist/MyTool.readme\n          category: util/misc\n          inject-version: true\n          # uploader-email is optional — if omitted, the action uses the\n          # email in the readme's Uploader: field as the FTP password.\n          # Pass it explicitly (e.g. from a secret) only if you want to\n          # override what's in the readme.\n```\n\n### Pull-request check (validate without uploading)\n\n```yaml\nname: Validate\non: [pull_request]\njobs:\n  readme:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - run: make dist\n      - id: aminet\n        uses: sidick/aminet-release-action@v1\n        with:\n          filename: dist/MyTool.lha\n          readme: dist/MyTool.readme\n          category: util/misc\n          validate-only: true\n          check-requires: true\n          check-replaces: true   # new in 1.1.0: HEAD-check Replaces: entries too\n\n      # Optional: fail the job if validation produced any warnings.\n      # (Validation errors already fail the action with exit 1; warnings\n      # don't, but the count is exposed as an output so you can decide.)\n      - if: steps.aminet.outputs.warnings != '0'\n        run: |\n          echo \"::error::Readme has ${{ steps.aminet.outputs.warnings }} warning(s).\"\n          exit 1\n```\n\n`uploader-email` is never consulted in `validate-only` mode (no upload happens), and is also unnecessary in upload mode whenever the readme's `Uploader:` field contains an email — see the input table below.\n\n## Inputs\n\n| Input | Required | Default | Description |\n|---|---|---|---|\n| `filename` | yes | — | Path to the file to upload. Aminet accepts archives (`.lha`, `.run`, `.zip`), tarballs (`.tar`, `.tar.gz`, `.tgz`, `.tar.bz2`), disk images (`.adf`, `.adz`), pictures (`.jpg`, `.png`, `.gif`), documents (`.pdf`, `.txt`), audio (`.ogg`, `.mp3`), and video (`.mpg`). |\n| `readme` | yes | — | Path to the Aminet-format `.readme` file. |\n| `category` | yes | — | Aminet category, e.g. `util/misc`, `dev/c`. Must match the `Type:` field in the readme. |\n| `uploader-email` | no | `''` | Your email address. Used as the FTP password for anonymous upload. If omitted, the action extracts the first email-like token from the readme's `Uploader:` field (plain `name@host`, `name@host (Name)`, or `Name \u003cname@host\u003e` are all accepted) and logs a `notice` showing which address it picked. The upload fails (exit 2) only if neither source yields an email. Ignored in `validate-only` mode. |\n| `inject-version` | no | `false` | If `true`, rewrites the readme's `Version:` field from the git tag (strips a leading `v`) before validation. Hard error if not run on a tag push. |\n| `validate-only` | no | `false` | If `true`, validate the readme and exit; skip upload and release-asset attachment. |\n| `check-requires` | no | `false` | If `true`, HTTP-HEAD each file-path entry in `Requires:` against `aminet.net`. 404 → error; other failures → warning. Off by default because it adds a network dependency at validation time. |\n| `check-replaces` | no | `false` | If `true`, same HEAD-based check applied to the `Replaces:` field. Wildcard entries (`*`, `?`) are skipped — they can't be HEAD-checked meaningfully. |\n| `ftp-host` | no | `main.aminet.net` | Accepts `host` or `host:port`. Override only for debugging — the default targets the real Aminet. |\n\n## Filename rules\n\nPer the wiki, upload filenames must be ≤ 30 characters and contain only `[A-Za-z0-9._-]`. Version numbers belong in the readme's `Version:` field, not the filename. The validator enforces both.\n\n## GitHub Releases\n\nWhen the workflow that calls the action is triggered by a **tag push** (`GITHUB_REF` starts with `refs/tags/`), the action looks for a matching GitHub Release and attaches the upload file and readme to it as release assets.\n\n**Lookup order:**\n1. Tag name verbatim (e.g. `v1.0.0`).\n2. Tag name with a leading `v` stripped (e.g. `1.0.0`).\n   This lets you tag with either convention; the action finds the release either way.\n\n**Required permission:** the workflow needs `contents: write` so the action's `GITHUB_TOKEN` can upload release assets. The release workflow example above sets this.\n\n**Graceful behaviour:**\n- **No matching release** → a `notice` is logged (\"No GitHub Release found for tag …; skipping asset attachment\"). The action still exits with the Aminet upload result; the missing release is not a failure.\n- **Asset upload fails** (transient API error, etc.) → a `warning` is logged. The action still exits successfully if the Aminet FTP upload succeeded — the release attachment is best-effort and never overrides the upload result.\n- **Not a tag push** → the release attachment step is skipped entirely. No API calls are made.\n- **`GITHUB_TOKEN` or `GITHUB_REPOSITORY` not in the environment** → a `warning` is logged and attachment is skipped.\n\nThe `release-attached` output (see below) is `true` only when both files were successfully attached.\n\n## Outputs\n\n| Output | Type | Description |\n|---|---|---|\n| `uploaded` | bool | `true` if the action actually uploaded the files to FTP. |\n| `release-attached` | bool | `true` if both files were attached to a matching GitHub Release. |\n| `errors` | int | Count of validation errors. |\n| `warnings` | int | Count of validation warnings. |\n| `filename` | string | Basename of the upload file (as it would land on Aminet). |\n| `readme` | string | Basename of the readme file. |\n\nReference them in downstream steps as `steps.\u003cid\u003e.outputs.\u003cname\u003e`:\n\n```yaml\n      - id: aminet\n        uses: sidick/aminet-release-action@v1\n        with:\n          filename: dist/MyTool.lha\n          readme: dist/MyTool.readme\n          category: util/misc\n          # uploader-email omitted — taken from the readme's Uploader: field\n\n      - if: steps.aminet.outputs.uploaded == 'true'\n        run: echo \"Shipped ${{ steps.aminet.outputs.filename }} to Aminet\"\n\n      - if: steps.aminet.outputs.uploaded == 'true' \u0026\u0026 steps.aminet.outputs.release-attached != 'true'\n        run: echo \"::warning::Uploaded to Aminet but couldn't attach to GitHub Release\"\n```\n\n## Exit codes\n\n| Code | Meaning |\n|---|---|\n| 0 | Success (or validation passed in `validate-only` mode) |\n| 1 | Validation failure |\n| 2 | Upload failure |\n\n## What's new in 1.1.0\n\n- **`uploader-email` is now optional.** When omitted, the action extracts the email from the readme's `Uploader:` field and logs a `notice` showing which address it picked. Workflows that don't want to manage a separate `AMINET_UPLOADER_EMAIL` secret can drop the input entirely.\n- **New `check-replaces` input.** Opt-in HEAD check for file-path entries in the readme's `Replaces:` field, mirroring `check-requires`. Wildcards (`*`, `?`) are skipped because they can't be HEAD-checked meaningfully.\n- **Action outputs.** `uploaded`, `release-attached`, `errors`, `warnings`, `filename`, and `readme` are now written to `$GITHUB_OUTPUT` on every run, so downstream steps can branch on results (e.g. notify Slack only when `uploaded == 'true'`).\n- **Workflow Summary tab.** Each run posts a small markdown table to `GITHUB_STEP_SUMMARY` showing validation result, upload target, and release-attach status — visible on the run's Summary tab without digging into logs.\n- **Bug fix: tags containing `/` (e.g. `release/1.0`).** GitHub Release lookup now URL-encodes the tag fully, so slashes no longer corrupt the API path.\n\n## How it works\n\nThe code under [`action/`](./action/) is the source of truth — module layout, validation rules, FTP procedure, and exit-code contract all live there:\n\n- `entrypoint.py` — input parsing and pipeline orchestration\n- `readme_validator.py` — header parsing, field rules, `inject_version`\n- `path_checker.py` — opt-in HEAD checks for `Requires:` / `Replaces:`\n- `ftp_uploader.py` — `lftp` subprocess wrapper\n- `github_output.py` — annotations, `$GITHUB_OUTPUT`, step summary\n- `github_release.py` — release lookup and asset upload via the GitHub API\n\nThe Aminet wiki is the upstream spec for the readme format and upload procedure: [The Readme file](https://wiki.aminet.net/The_Readme_file), [Uploading instructions](https://wiki.aminet.net/Uploading_instructions).\n\nFor local development, [`CLAUDE.md`](./CLAUDE.md) documents the Makefile targets (`make test`, `make smoke`, `make ci`).\n\n## License\n\n[MIT](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsidick%2Faminet-release-action","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsidick%2Faminet-release-action","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsidick%2Faminet-release-action/lists"}